├── .dockerignore ├── .env.example ├── .gitignore ├── .vscode └── launch.json ├── Dockerfile ├── Readme.md ├── backend ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── nest-cli.json ├── package-lock.json ├── package.json ├── src │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── auth │ │ ├── auth.module.ts │ │ ├── controllers │ │ │ ├── auth.controller.ts │ │ │ ├── google-oauth.controller.ts │ │ │ └── twitter-oauth.controller.ts │ │ ├── dtos │ │ │ ├── forgot-password.dto.ts │ │ │ ├── register-user.dto.ts │ │ │ ├── reset-password.dto.ts │ │ │ ├── update-password.dto.ts │ │ │ └── verify-email-token.dto.ts │ │ ├── entities │ │ │ ├── forgot-password-token.entity.ts │ │ │ └── session.entity.ts │ │ ├── guards │ │ │ ├── google-auth.guard.ts │ │ │ ├── local.guard.ts │ │ │ ├── logged-in.guard.ts │ │ │ └── twitter-auth.guard.ts │ │ ├── providers │ │ │ └── serialization.provider.ts │ │ ├── services │ │ │ ├── auth.service.ts │ │ │ ├── oauth.service.ts │ │ │ └── password-auth.service.ts │ │ └── strategies │ │ │ ├── google.strategy.ts │ │ │ ├── local.strategy.ts │ │ │ └── twitter.strategy.ts │ ├── database │ │ └── database.module.ts │ ├── logger │ │ └── logger.module.ts │ ├── mail │ │ ├── mail.module.ts │ │ ├── mail.service.ts │ │ └── templates │ │ │ ├── confirmation.hbs │ │ │ └── reset_password.hbs │ ├── main.ts │ └── user │ │ ├── interfaces │ │ └── UserInterface.ts │ │ ├── user.entity.ts │ │ ├── user.module.ts │ │ └── user.service.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json ├── docker-compose.yml ├── frontend ├── .browserslistrc ├── .env.local.example ├── .gitignore ├── babel.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── Layouts │ │ ├── AuthLayout.vue │ │ ├── BaseLayout.vue │ │ └── GuestLayout.vue │ ├── assets │ │ ├── logo.png │ │ └── tailwind.css │ ├── components │ │ ├── Buttons │ │ │ ├── BurgerButton.vue │ │ │ ├── ButtonBase.vue │ │ │ ├── PrimaryButton.vue │ │ │ └── Socials │ │ │ │ ├── GoogleAuthButton.vue │ │ │ │ └── TwitterAuthButton.vue │ │ ├── Form │ │ │ ├── Form.vue │ │ │ ├── FormCard.vue │ │ │ ├── Input.vue │ │ │ ├── Select.vue │ │ │ └── TextArea.vue │ │ ├── Modals │ │ │ ├── ModalBase.vue │ │ │ └── StandardModal.vue │ │ ├── Navigation │ │ │ ├── AuthenticatedNavbar.vue │ │ │ ├── DefaultNavbar.vue │ │ │ └── Navbar │ │ │ │ ├── MobileNavbarItem.vue │ │ │ │ ├── NavbarItem.vue │ │ │ │ └── ProfileMenu │ │ │ │ ├── ProfileIcon.vue │ │ │ │ └── ProfileMenu.vue │ │ ├── Typography │ │ │ ├── CodeSnippet.vue │ │ │ ├── MainHeadline.vue │ │ │ ├── Paragraph.vue │ │ │ ├── SectionHeadline.vue │ │ │ ├── SmallLabel.vue │ │ │ └── SubHeader.vue │ │ └── Widgets │ │ │ ├── Alert.vue │ │ │ ├── Badge.vue │ │ │ ├── Bar │ │ │ ├── BarElement.vue │ │ │ └── BarWrapper.vue │ │ │ ├── Card.vue │ │ │ ├── Divider.vue │ │ │ └── Spinner │ │ │ └── Spinner.vue │ ├── main.js │ ├── router │ │ └── index.js │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ └── auth.js │ └── views │ │ ├── Auth │ │ ├── Login.vue │ │ └── Register.vue │ │ ├── Dashboard.vue │ │ └── Home.vue └── tailwind.config.js └── package-lock.json /.dockerignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niclas-timm/nestjs-vue-boilerplate/2faed652a33dc89a2b679197d056328407ebe783/.dockerignore -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Database 2 | DB_CONNECTION=postgres 3 | DB_HOST=database 4 | DB_PORT=3306 5 | DB_DATABASE=vuenestboilerplate 6 | DB_USERNAME=coolguy 7 | DB_PASSWORD=123456 8 | 9 | # Mail 10 | MAIL_MAILER=smtp 11 | MAIL_HOST=mailhog_app 12 | MAIL_PORT=1025 13 | MAIL_USERNAME=null 14 | MAIL_PASSWORD=null 15 | MAIL_ENCRYPTION=null 16 | MAIL_FROM_ADDRESS=email@example.com 17 | MAIL_FROM_NAME= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | # Environment variables 38 | .env 39 | .env.local 40 | 41 | postgres-data/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "attach", 7 | "name": "Debug: app-name", 8 | "remoteRoot": "/var/www/html", 9 | "localRoot": "${workspaceFolder}/backend", 10 | "protocol": "inspector", 11 | "port": 9229, 12 | "restart": true, 13 | "address": "0.0.0.0", 14 | "skipFiles": ["/**"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /var/www/html 4 | 5 | RUN apk update 6 | RUN apk upgrade 7 | RUN apk add bash 8 | 9 | # RUN npm cache clean --force && rm -rf node_modules 10 | 11 | COPY ./backend . 12 | 13 | # CMD [ "npm", "install" ] -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Vue NestJs Boilerplate 2 | 3 | Have any questions? Want to get in contact? Hit me up on [Twitter](https://twitter.com/niclas_timm) :bird: ! 4 | 5 | A boilerplate for a Full-Stack application using NestJS 8 for the backend and Vue 3 for the frontend. 6 | 7 | It includes: 8 | 9 | - :closed_lock_with_key: Authentication (including oAuth for Google and Twitter, Email verification, password reset etc.) 10 | - :cd: Rich configuration possibilities via environment variables 11 | - :whale: A dockerized environment (usage not mandatory but recommended): Backend & frontend server, Postgres, pgadmin, Redis, Mailhog 12 | - :e-mail: Sending email + HTML email templates 13 | - :bug: Debugging setup for VS Code for the backend 14 | - :cloud: TailwindCSS integration 15 | 16 | # Quick start 17 | 18 | ## Install dependencies 19 | 20 | After cloning or downloading the repo, you first need to install the dependencies on both the backend and frontend: 21 | `cd backend; npm install` 22 | `cd ../frontend; npm install` 23 | 24 | ## Environment variables 25 | 26 | You can configure large parts of the app via environment variables. There are three environment files: 27 | 28 | - :arrow_left: Backend environment 29 | - :arrow_right: Frontend environment 30 | - :whale: Docker environment (optional) 31 | 32 | ### Backend 33 | 34 | Execute the following commands: 35 | 36 | ``` 37 | cd backend && cp .env.example .env 38 | ``` 39 | 40 | Fill in the values as needed. If you plan to use the **dockerized** environment provided in this repo, there is **no need to change the database and email configuration** 41 | 42 | ### Frontend 43 | 44 | Navigate into the frontend directory and execute the following commands: 45 | 46 | ``` 47 | cp .env.local.example .env.local 48 | ``` 49 | 50 | There is not much to do, actually. Just make sure that you enter the right `VUE_APP_API_URL` without a trailing slash. By default, NestJS runs on `http://localhost:3000`, so you probably don't need to change the default value in `.env.local`. 51 | 52 | ### Docker environment (optional) 53 | 54 | > :bulb: **Not using Docker?** 55 | > You can ignore this section if you don't want to use Docker for your development environment. 56 | 57 | First, in the root directory, execute: 58 | 59 | ``` 60 | cp .env.example .env 61 | ``` 62 | 63 | Afterwards, you can adjust the values as you wish. They will be used in the `docker-compose.yml`file for configuring your containers. However, you can also stick to the default values as they will get the job done :muscle: . 64 | 65 | > :warning: **Docker containers not suited for production** 66 | > The provided Docker configuration is not secure enough for a production environment. It is only supposed to be used during development. 67 | 68 | ## Exploring the app 69 | 70 | Once you're set up, you can visit your frontend URL (usually `http://localhost:8080`). But there won't be too much to see. So let's move on to `/register`! There, you'll be able to register for an account. Once you finish that, you will be redirected to `/dashboard`. You can only access this route when you are logged in. 71 | 72 | ## Authentication :closed_lock_with_key: 73 | 74 | The app comes with a couple of different ways a user can login/register: 75 | 76 | - The traditional (local) way by providing an email address and a password 77 | - Google oAuth 78 | - Twitter oAuth 79 | 80 | ### Authentication backend :arrow_left: 81 | 82 | The authentication on the backend is primarily handled in the `/backend/src/auth` module. However, it also requires the `/backend/src/user` and `/backend/src/mail` module to work properly. 83 | 84 | #### The user module 85 | 86 | The `user` module is solely concerned with the user entity that is stored in the database. This means: 87 | 88 | - Defining the user entity (`/backend/src/user/user.entity.ts`) 89 | - Providing a service to query the user table (`/backend/src/user/user.service.ts`) 90 | 91 | #### The auth module 92 | 93 | The `auth` module handles all the authentication and authorization of users. This means: 94 | 95 | - (oAuth) Login & registration 96 | - Authorization / protecting routes 97 | - Email verification 98 | - Password reset 99 | 100 | The module uses [Passport](https://www.passportjs.org/) under the hood with a session based authenctication approach. It takes advantage of the following strategies: 101 | 102 | - Local strategy 103 | - Google Passport oAuth 2.0 104 | - Twitter Passport oAuth 105 | 106 | The boilerplate also takes advantage of the thin wrapper packages that NestJS offers for passport. Thus, the authentication/authorization logic primarily happens in `/backend/src/auth/controllers`, `/backend/src/auth/guards` and `/backend/src/auth/strategies` 107 | 108 | **Sessions** 109 | Passport offers a variety of different authentication strategies. This boilerplate uses a session based authentication approach, where the sessions are stored in the Postgres database in a `sessions` table. To achieve this, the app uses the `express-session` and `connect-typeorm`packages. 110 | 111 | **oAuth** 112 | 113 | > :bulb: **You must register your apps on Google and Twitter** 114 | > You can only use oAuth if you register your apps on Google and Twitter beforehand and paste the correct values into the /backend/.env file. 115 | 116 | OAuth gets kind of tricky when you have a decoupled frontend and backend. Here is how the app handles it: 117 | 118 | - On the frontend, click on a social login button 119 | - This button redirects you to `/auth/twitter` or `/auth/google` 120 | - These domains, in turn, automatically redirect you to the respective authentication page of the OAuth provider 121 | - After successfully authenticating there, the authentication provider redirects you back to `/auth/twitter/callback` or `/auth/google/callback`. 122 | - There, passport does its magic and retrieves the corresponding user information by using the access token from the url to ping the API of the OAuth provider. 123 | - Next, a user with the corresponding email address will be searched in the database and the user will either be logged in or a new user will be created. 124 | - Finally, the user gets redirected to `/auth/twitter/callback` or `/auth/google/callback`. 125 | 126 | > :warning: **The callback matters** 127 | > Please make sure to enter a callback in the form of /auth/{twitter_or_google}/callback in the settings of the respective providers. The boilerplate wont work correctly otherwise. 128 | 129 | #### The mail module 130 | 131 | The mail `/backend/src/module` is required for sending email verification and password reset mails. It leverages the awesome [@nestjs-modules/mailer](https://www.npmjs.com/package/@nestjs-modules/mailer) package, which enables the following things: 132 | 133 | - Creating HTML-Email templates with handlebars 134 | - Dynamically injecting values into these templates 135 | - A nicely formatted API to actually send the emails 136 | 137 | **Templates** 138 | Email templates live under `/templates`. by default, you'll find two. One email verification/welcome template and a password reset template. They are written in handlebars so that we can later dynamically inject values. 139 | The methods for actually sending the emails live in `mail.service.ts`. 140 | So, if you want to create and send additional mails, you need to do the following: 141 | 142 | - Create a new template under `/templates` 143 | - Create a new method in `mail.service.ts`. In the `context` option, choose the values for the variables you declared in the template file 144 | - Call the newly created method somewhere else in your code in order so send the mail. 145 | 146 | ## Authentication frontend :arrow_right: 147 | 148 | On the frontend, authentication is primarily handled in the Vuex store, specifically under `/frontend/src/store/modules/auth.js`. There, all the api calls are made and the user data and potential errors are stored in the Vuex store. 149 | 150 | ### Handling OAuth on the frontend 151 | 152 | As described above, when clicking on "Login with Google/Twitter" the user will be: 153 | 154 | - Directed to the authentication page of Google or Twitter 155 | - Redirected to the backend, specifically to `/auth/{google_or_twitter}/callback`, where Passport does some magic 156 | - Finally, the user will be redirected to the frontend 157 | 158 | ## Logging :memo: 159 | 160 | By default, NestJS logs everything to the console. However, especially in production, it might be useful to receive all errors in a file, like `error.log`. 161 | To achieve this, we use the [nest-winston](https://github.com/gremo/nest-winston) package. 162 | 163 | ### Enabling logging 164 | 165 | By default, logging is disabled. To change this, go to `/backend/src/.env` and change the `ENABLE_LOGGING`from `false` to `true`. 166 | Also in the `.env` file, you can configure the file path for logging via the `ERROR_LOG_FILE_PATH` variable, which is where the logging information will be stored. By default it is `error.log`. 167 | 168 | ### Configure logging 169 | 170 | You can configure the logging behaviour in the logging module under `/backend/src/logger/logger.module.ts`. By default, only error messages will be logged to the file you declared in the `.env` file (see above). If you also want other messages like info messages to be logged, you have to manually add them to `logger.module.ts`. For more information on how to do that check out the [nest-winston documentation](https://github.com/gremo/nest-winston). 171 | 172 | ### Using the custom logger 173 | 174 | To log a message, you must do the following: 175 | 176 | 1. Add the logger service as a dependeny. For example in a service file: 177 | 178 | ```ts 179 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; 180 | import { Injectable, LoggerService } from '@nestjs/common'; 181 | 182 | export class MyService { 183 | constructor( 184 | @Inject(WINSTON_MODULE_NEST_PROVIDER) 185 | private readonly logger: LoggerService, 186 | ) {} 187 | } 188 | ``` 189 | 190 | To then log a message, do: 191 | 192 | ```ts 193 | this.logger.error('My error message goes here'); 194 | ``` 195 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | # General 2 | API_URL=http://localhost:3000/ 3 | FRONTEND_URL=http://localhost:8080/ 4 | 5 | # Database 6 | DB_CONNECTION=mysql 7 | DB_HOST=database 8 | DB_PORT=3306 9 | DB_DATABASE=nesttest 10 | DB_USERNAME=niclas 11 | DB_PASSWORD=123456 12 | 13 | # Email 14 | MAIL_MAILER=smtp 15 | MAIL_HOST=mailhog_app 16 | MAIL_PORT=1025 17 | MAIL_USERNAME=null 18 | MAIL_PASSWORD=null 19 | MAIL_ENCRYPTION=null 20 | MAIL_FROM_ADDRESS=niclastimmdev@gmail.com 21 | MAIL_FROM_NAME= 22 | 23 | # JWT + Session 24 | JWT_SECRET=MY_COOL_SECRET 25 | SESSION_SECRET=MY_SESSION_SECRET 26 | 27 | # Logging 28 | ENABLE_LOGGING=true 29 | ERROR_LOG_FILE_PATH=error.log 30 | 31 | # Google oAuth 32 | GOOGLE_CLIENT_ID= 33 | GOOGLE_SECRET= 34 | GOOGLE_CALLBACK= 35 | 36 | # Twitter oAuth 37 | TWITTER_CLIENT_ID= 38 | TWITTER_SECRET= 39 | TWITTER_CALLBACK= -------------------------------------------------------------------------------- /backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | # Environment variables 38 | .env 39 | .env.local -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | 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). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /backend/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "assets": [ 6 | "mail/templates/**/*" 7 | ], 8 | "watchAssets": true 9 | } 10 | } -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test2", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug 0.0.0.0:9229 --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs-modules/mailer": "^1.6.0", 25 | "@nestjs/common": "^8.0.0", 26 | "@nestjs/config": "^1.1.5", 27 | "@nestjs/core": "^8.0.0", 28 | "@nestjs/jwt": "^8.0.0", 29 | "@nestjs/passport": "^8.0.1", 30 | "@nestjs/platform-express": "^8.0.0", 31 | "@nestjs/typeorm": "^8.0.2", 32 | "bcryptjs": "^2.4.3", 33 | "class-transformer": "^0.5.1", 34 | "class-validator": "^0.13.2", 35 | "connect-typeorm": "^1.1.4", 36 | "express-session": "^1.17.2", 37 | "gravatar": "^1.8.2", 38 | "jsonwebtoken": "^8.5.1", 39 | "mysql2": "^2.3.3", 40 | "nest-winston": "^1.6.2", 41 | "nodemailer": "^6.7.2", 42 | "passport": "^0.5.2", 43 | "passport-google-oauth20": "^2.0.0", 44 | "passport-jwt": "^4.0.0", 45 | "passport-local": "^1.0.0", 46 | "passport-twitter": "^1.0.4", 47 | "pg": "^8.7.1", 48 | "reflect-metadata": "^0.1.13", 49 | "rimraf": "^3.0.2", 50 | "rxjs": "^7.2.0", 51 | "typeorm": "^0.2.41", 52 | "winston": "^3.3.3" 53 | }, 54 | "devDependencies": { 55 | "@nestjs/cli": "^8.0.0", 56 | "@nestjs/schematics": "^8.0.0", 57 | "@nestjs/testing": "^8.0.0", 58 | "@types/bcrypt": "^5.0.0", 59 | "@types/bcryptjs": "^2.4.2", 60 | "@types/express": "^4.17.13", 61 | "@types/express-session": "^1.17.4", 62 | "@types/jest": "27.0.2", 63 | "@types/node": "^16.0.0", 64 | "@types/passport-google-oauth20": "^2.0.11", 65 | "@types/passport-local": "^1.0.34", 66 | "@types/supertest": "^2.0.11", 67 | "@typescript-eslint/eslint-plugin": "^5.0.0", 68 | "@typescript-eslint/parser": "^5.0.0", 69 | "eslint": "^8.0.1", 70 | "eslint-config-prettier": "^8.3.0", 71 | "eslint-plugin-prettier": "^4.0.0", 72 | "jest": "^27.2.5", 73 | "prettier": "^2.3.2", 74 | "source-map-support": "^0.5.20", 75 | "supertest": "^6.1.3", 76 | "ts-jest": "^27.0.3", 77 | "ts-loader": "^9.2.3", 78 | "ts-node": "^10.0.0", 79 | "tsconfig-paths": "^3.10.1", 80 | "typescript": "^4.3.5" 81 | }, 82 | "jest": { 83 | "moduleFileExtensions": [ 84 | "js", 85 | "json", 86 | "ts" 87 | ], 88 | "rootDir": "src", 89 | "testRegex": ".*\\.spec\\.ts$", 90 | "transform": { 91 | "^.+\\.(t|j)s$": "ts-jest" 92 | }, 93 | "collectCoverageFrom": [ 94 | "**/*.(t|j)s" 95 | ], 96 | "coverageDirectory": "../coverage", 97 | "testEnvironment": "node" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /backend/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { UserModule } from './user/user.module'; 5 | import { AuthModule } from './auth/auth.module'; 6 | import { ConfigModule } from '@nestjs/config'; 7 | import { MailModule } from './mail/mail.module'; 8 | import { LoggerModule } from './logger/logger.module'; 9 | import { DatabaseModule } from './database/database.module'; 10 | 11 | @Module({ 12 | imports: [ 13 | UserModule, 14 | AuthModule, 15 | MailModule, 16 | ConfigModule.forRoot({ isGlobal: true }), 17 | DatabaseModule, 18 | LoggerModule, 19 | ], 20 | controllers: [AppController], 21 | providers: [AppService], 22 | }) 23 | export class AppModule {} 24 | -------------------------------------------------------------------------------- /backend/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { PasswordService } from './services/password-auth.service'; 2 | import { OAuthService } from './services/oauth.service'; 3 | import { AuthSerializer } from './providers/serialization.provider'; 4 | import { TwitterStrategy } from './strategies/twitter.strategy'; 5 | import { TwitterOAuthController } from './controllers/twitter-oauth.controller'; 6 | import { ForgotPasswordToken } from './entities/forgot-password-token.entity'; 7 | import { TypeOrmModule } from '@nestjs/typeorm'; 8 | import { MailModule } from './../mail/mail.module'; 9 | import { JwtModule } from '@nestjs/jwt'; 10 | import { LocalStrategy } from './strategies/local.strategy'; 11 | import { UserModule } from './../user/user.module'; 12 | import { Module } from '@nestjs/common'; 13 | import { AuthService } from './services/auth.service'; 14 | import { PassportModule } from '@nestjs/passport'; 15 | import { AuthController } from './controllers/auth.controller'; 16 | import { ConfigModule } from '@nestjs/config'; 17 | import { GoogleStrategy } from './strategies/google.strategy'; 18 | import { GoogleOAuthController } from './controllers/google-oauth.controller'; 19 | 20 | @Module({ 21 | providers: [ 22 | AuthService, 23 | OAuthService, 24 | PasswordService, 25 | LocalStrategy, 26 | GoogleStrategy, 27 | TwitterStrategy, 28 | AuthSerializer, 29 | ], 30 | imports: [ 31 | UserModule, 32 | PassportModule.register({ 33 | session: true, 34 | }), 35 | MailModule, 36 | ConfigModule.forRoot(), 37 | JwtModule.register({ 38 | secret: process.env.JWT_SECRET, 39 | signOptions: { expiresIn: '1000000s' }, 40 | }), 41 | TypeOrmModule.forFeature([ForgotPasswordToken]), 42 | ], 43 | exports: [AuthService, OAuthService, PasswordService], 44 | controllers: [AuthController, GoogleOAuthController, TwitterOAuthController], 45 | }) 46 | export class AuthModule {} 47 | -------------------------------------------------------------------------------- /backend/src/auth/controllers/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { PasswordService } from './../services/password-auth.service'; 2 | import { LoggedInGuard } from './../guards/logged-in.guard'; 3 | import { VerifyEmailTokenDto } from '../dtos/verify-email-token.dto'; 4 | import { UpdatePasswordDto } from '../dtos/update-password.dto'; 5 | import { AuthService } from '../services/auth.service'; 6 | import { LocalGuard } from '../guards/local.guard'; 7 | import { 8 | Body, 9 | Controller, 10 | Get, 11 | Param, 12 | Patch, 13 | Post, 14 | Req, 15 | Request, 16 | UseGuards, 17 | UsePipes, 18 | ValidationPipe, 19 | } from '@nestjs/common'; 20 | import { RegisterUserDto } from '../dtos/register-user.dto'; 21 | import { ForgotPasswordDto } from '../dtos/forgot-password.dto'; 22 | import { ResetPasswordDto } from '../dtos/reset-password.dto'; 23 | 24 | @Controller('auth') 25 | export class AuthController { 26 | constructor( 27 | private readonly authService: AuthService, 28 | private readonly passwordService: PasswordService, 29 | ) {} 30 | 31 | /** 32 | * Log user in. 33 | * 34 | * @param {Request} req 35 | * The request object. 36 | * 37 | * @returns User 38 | */ 39 | @UseGuards(LocalGuard) 40 | @Post('login') 41 | async login(@Request() req) { 42 | return req.session; 43 | } 44 | 45 | /** 46 | * Register new user. 47 | * 48 | * @param {RegisterUserDto} registerUserDto 49 | * The data (name, email, password) of the new user. 50 | * 51 | * @returns 52 | */ 53 | @Post('register') 54 | @UsePipes(new ValidationPipe({ transform: true })) 55 | async registerUser(@Body() registerUserDto: RegisterUserDto, @Request() req) { 56 | const result = await this.authService.registerUser(registerUserDto); 57 | if (result.user) { 58 | req.login(result.user, function (err) { 59 | if (err) { 60 | throw new Error( 61 | 'Sorry, somethin went wrong. We could register but sign you in.', 62 | ); 63 | } 64 | return req.session; 65 | }); 66 | } 67 | } 68 | 69 | /** 70 | * Get data of the current user. 71 | * 72 | * @param {Request} req 73 | * The request object. 74 | * 75 | * @returns 76 | */ 77 | @UseGuards(LoggedInGuard) 78 | @Get('user') 79 | async getUser(@Request() req): Promise { 80 | delete req.user.password; 81 | return req.user; 82 | } 83 | 84 | @UseGuards(LoggedInGuard) 85 | @Get('logout') 86 | logout(@Request() req) { 87 | req.session.destroy(); 88 | return req.logOut(); 89 | } 90 | 91 | /** 92 | * Verify email address of user. 93 | * 94 | * @param {Param} params 95 | * A token holding data about the validation process. 96 | * 97 | * @returns 98 | */ 99 | @UsePipes(new ValidationPipe({ transform: true })) 100 | @Get('email/verify/:token') 101 | async verifyEmail(@Param() params: VerifyEmailTokenDto) { 102 | return this.authService.verifyEmail(params.token); 103 | } 104 | 105 | /** 106 | * Update password of a user. 107 | * 108 | * @param {Request} req 109 | * The request object. 110 | * 111 | * @param {UpdatePasswordDto} body 112 | * Information about the new password. 113 | * 114 | * @returns 115 | */ 116 | @UseGuards(LoggedInGuard) 117 | @UsePipes(new ValidationPipe({ transform: true })) 118 | @Patch('password/update') 119 | async updatePassword(@Request() req, @Body() body: UpdatePasswordDto) { 120 | return await this.passwordService.changePassword( 121 | req.user, 122 | body.oldPassword, 123 | body.newPassword, 124 | ); 125 | } 126 | 127 | /** 128 | * Send email to user with a reset password link. 129 | * 130 | * @param {ForgotPasswordDto} body 131 | */ 132 | @UsePipes(new ValidationPipe({ transform: true })) 133 | @Post('password/forgotlink') 134 | async sendForgotPasswordLink(@Body() body: ForgotPasswordDto) { 135 | this.passwordService.sendForgotPasswordLink(body.email); 136 | } 137 | 138 | /** 139 | * Reset password of a user. 140 | * 141 | * @param {ResetPasswordDto} body 142 | * Data about the new password. 143 | */ 144 | @Post('password/reset') 145 | @UsePipes(new ValidationPipe({ transform: true })) 146 | async resetPassword(@Body() body: ResetPasswordDto) { 147 | this.passwordService.resetPassword(body.token, body.newPassword); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /backend/src/auth/controllers/google-oauth.controller.ts: -------------------------------------------------------------------------------- 1 | import { OAuthService } from './../services/oauth.service'; 2 | import { Controller, Get, Req, Request, UseGuards, Res } from '@nestjs/common'; 3 | import { GoogleOAuthGuard } from '../guards/google-auth.guard'; 4 | 5 | @Controller('auth/google') 6 | export class GoogleOAuthController { 7 | @Get() 8 | @UseGuards(GoogleOAuthGuard) 9 | async googleAuth(@Req() req) {} 10 | 11 | @Get('callback') 12 | @UseGuards(GoogleOAuthGuard) 13 | async googleAuthRedirect(@Request() req, @Res() res) { 14 | res.redirect(`${process.env.FRONTEND_URL}/dashboard`); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/auth/controllers/twitter-oauth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Req, UseGuards, Redirect, Res } from '@nestjs/common'; 2 | import { TwitterOAuthGuard } from '../guards/twitter-auth.guard'; 3 | 4 | @Controller('auth/twitter') 5 | export class TwitterOAuthController { 6 | @Get() 7 | @UseGuards(TwitterOAuthGuard) 8 | async googleAuth(@Req() req) {} 9 | 10 | @Get('callback') 11 | @UseGuards(TwitterOAuthGuard) 12 | async twitterAuthRedirect(@Req() req, @Res() res) { 13 | res.redirect(`${process.env.FRONTEND_URL}/dashboard`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/auth/dtos/forgot-password.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail } from 'class-validator'; 2 | 3 | export class ForgotPasswordDto { 4 | @IsEmail() 5 | email: string; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/auth/dtos/register-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty } from 'class-validator'; 2 | 3 | export class RegisterUserDto { 4 | @IsEmail() 5 | email: string; 6 | 7 | @IsNotEmpty() 8 | name: string; 9 | 10 | @IsNotEmpty() 11 | password: string; 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/auth/dtos/reset-password.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty } from 'class-validator'; 2 | 3 | export class ResetPasswordDto { 4 | @IsNotEmpty() 5 | token: string; 6 | 7 | @IsNotEmpty() 8 | newPassword: string; 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/auth/dtos/update-password.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | 3 | export class UpdatePasswordDto { 4 | @IsNotEmpty() 5 | oldPassword: string; 6 | 7 | @IsNotEmpty() 8 | newPassword: string; 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/auth/dtos/verify-email-token.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | 3 | export class VerifyEmailTokenDto { 4 | @IsNotEmpty() 5 | token: string; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/auth/entities/forgot-password-token.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | Entity, 5 | PrimaryGeneratedColumn, 6 | UpdateDateColumn, 7 | } from 'typeorm'; 8 | 9 | @Entity() 10 | export class ForgotPasswordToken { 11 | @PrimaryGeneratedColumn() 12 | id: number; 13 | 14 | @Column() 15 | email: string; 16 | 17 | @Column({ length: 400 }) 18 | token: string; 19 | 20 | @CreateDateColumn() 21 | created_at: Date; 22 | 23 | @UpdateDateColumn() 24 | updated_at: Date; 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/auth/entities/session.entity.ts: -------------------------------------------------------------------------------- 1 | import { ISession } from 'connect-typeorm'; 2 | import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; 3 | 4 | @Entity({ name: 'sessions' }) 5 | export class TypeORMSession implements ISession { 6 | @Index() 7 | @Column('bigint') 8 | expiredAt: number; 9 | 10 | @PrimaryColumn('varchar', { length: 255 }) 11 | id: string; 12 | 13 | @Column('text') 14 | json: string; 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/auth/guards/google-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ExecutionContext } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class GoogleOAuthGuard extends AuthGuard('google') { 6 | async canActivate(context: ExecutionContext) { 7 | const activate = (await super.canActivate(context)) as boolean; 8 | const request = context.switchToHttp().getRequest(); 9 | await super.logIn(request); 10 | return activate; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/auth/guards/local.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class LocalGuard extends AuthGuard('local') { 6 | async canActivate(context: ExecutionContext): Promise { 7 | const result = (await super.canActivate(context)) as boolean; 8 | await super.logIn(context.switchToHttp().getRequest()); 9 | return result; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/auth/guards/logged-in.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class LoggedInGuard implements CanActivate { 5 | async canActivate(context: ExecutionContext): Promise { 6 | const req = context.switchToHttp().getRequest(); 7 | return req.isAuthenticated(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/auth/guards/twitter-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ExecutionContext } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class TwitterOAuthGuard extends AuthGuard('twitter') { 6 | async canActivate(context: ExecutionContext): Promise { 7 | const result = (await super.canActivate(context)) as boolean; 8 | const login = await super.logIn(context.switchToHttp().getRequest()); 9 | 10 | return result; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/auth/providers/serialization.provider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportSerializer } from '@nestjs/passport'; 3 | import { User } from 'src/user/user.entity'; 4 | import { UserService } from 'src/user/user.service'; 5 | 6 | @Injectable() 7 | export class AuthSerializer extends PassportSerializer { 8 | constructor(private readonly userService: UserService) { 9 | super(); 10 | } 11 | serializeUser(user: User, done: (err: Error, user: { id: any }) => void) { 12 | done(null, { id: user.id }); 13 | } 14 | 15 | async deserializeUser( 16 | payload: { id: number }, 17 | done: (err: Error, user: Omit) => void, 18 | ) { 19 | const user = await this.userService.findById(payload.id); 20 | 21 | done(null, user); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/auth/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { RegisterUserDto } from './../dtos/register-user.dto'; 2 | import { MailService } from '../../mail/mail.service'; 3 | import { UserService } from '../../user/user.service'; 4 | import { 5 | BadRequestException, 6 | ConflictException, 7 | Inject, 8 | Injectable, 9 | LoggerService, 10 | UnauthorizedException, 11 | } from '@nestjs/common'; 12 | import * as bcrypt from 'bcryptjs'; 13 | import * as jwt from 'jsonwebtoken'; 14 | import { User } from 'src/user/user.entity'; 15 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; 16 | 17 | @Injectable() 18 | export class AuthService { 19 | constructor( 20 | private userService: UserService, 21 | private mailService: MailService, 22 | @Inject(WINSTON_MODULE_NEST_PROVIDER) 23 | private readonly logger: LoggerService, 24 | ) {} 25 | 26 | /** 27 | * Check user by credentials. 28 | * 29 | * @param {string} email 30 | * The email of the user. 31 | * @param {string} password 32 | * The plain password of the user 33 | * 34 | * @returns Promise | null 35 | * The user object. 36 | */ 37 | async validateUser(email: string, password: string): Promise | null { 38 | // Find the user by email from database and also load the password. 39 | const user = await this.userService.find({ email }, true); 40 | 41 | if (!user) { 42 | throw new UnauthorizedException('Email or password incorrect.'); 43 | } 44 | 45 | // Accounts that are registered via oAuth should not be accessible via local signin. 46 | if (user.social_channel) { 47 | throw new UnauthorizedException('Email or password incorrect.'); 48 | } 49 | 50 | const isPasswordCorrect = await bcrypt.compare(password, user.password); 51 | 52 | if (!isPasswordCorrect) { 53 | throw new UnauthorizedException('Email or password incorrect.'); 54 | } 55 | 56 | // Remove the password again and send it to client. 57 | delete user.password; 58 | return user; 59 | } 60 | 61 | /** 62 | * Register a new user. 63 | * 64 | * @param {object} user 65 | * The data (name, email, password) of the new user. 66 | * 67 | * @returns {object} 68 | * The nw user. 69 | */ 70 | async registerUser(user: RegisterUserDto) { 71 | const existingUser = await this.userService.find({ email: user.email }); 72 | if (existingUser) { 73 | throw new ConflictException('Email already taken'); 74 | } 75 | const hashedPassword = await bcrypt.hash(user.password, 8); 76 | const res = await this.userService.createUser({ 77 | name: user.name, 78 | email: user.email, 79 | password: hashedPassword, 80 | }); 81 | 82 | delete res.password; 83 | 84 | // Send email. 85 | this.sendEmailVerificationMail(res); 86 | 87 | return { 88 | user: res, 89 | }; 90 | } 91 | 92 | /** 93 | * Send email verification email to user. 94 | * 95 | * @param {User} user 96 | * The user we want to send the email to 97 | */ 98 | private sendEmailVerificationMail(user: User): void { 99 | // Create JWT that holds the users email as payload and expires in 14 days. 100 | const token = jwt.sign({ ...user }, process.env.JWT_SECRET, { 101 | expiresIn: 60 * 60 * 24 * 14, 102 | }); 103 | 104 | // The url the user can click in the mail in order to verify the email address. 105 | const url = `${process.env.FRONTEND_URL}/auth/email/verify/${token}`; 106 | 107 | // Use the mailService to send the mail. 108 | this.mailService.sendUserConfirmation(user, 'BlaBla', url); 109 | } 110 | 111 | /** 112 | * Verify the email address of a user. 113 | * 114 | * @param {string} token 115 | * The token that holds the validation information. 116 | * 117 | * @returns 118 | */ 119 | async verifyEmail(token: string): Promise { 120 | // Validate token. Will throw error if it's not valid. 121 | let userFromTokenPayload: any; 122 | try { 123 | userFromTokenPayload = jwt.verify(token, process.env.JWT_SECRET); 124 | } catch (error) { 125 | throw new BadRequestException('Invalid token'); 126 | } 127 | 128 | // Update email verification status. 129 | const updatedUser = await this.userService.updateUser( 130 | userFromTokenPayload.id, 131 | { 132 | email_verified: true, 133 | }, 134 | ); 135 | 136 | return updatedUser; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /backend/src/auth/services/oauth.service.ts: -------------------------------------------------------------------------------- 1 | import { UserService } from './../../user/user.service'; 2 | import { BadRequestException, Injectable } from '@nestjs/common'; 3 | import { User } from 'src/user/user.entity'; 4 | 5 | @Injectable() 6 | export class OAuthService { 7 | constructor(private readonly userService: UserService) {} 8 | 9 | /** 10 | * Log in or register via oAuth. 11 | * 12 | * @param {Request} req 13 | * The request object. 14 | * 15 | * @returns 16 | */ 17 | async socialLogin(req: { user: any; socialChannel: string }) { 18 | // Passports middleware adds the user to the request object. 19 | if (!req.user) { 20 | throw new BadRequestException('No account presented.'); 21 | } 22 | 23 | // Check if there is a user with this email already: 24 | const existingUser: User = await this.userService.find({ 25 | email: req.user.email, 26 | }); 27 | if (existingUser) { 28 | return existingUser; 29 | } 30 | 31 | const newUser = await this.userService.createUser({ 32 | ...req.user, 33 | social_channel: req.socialChannel, 34 | email_verified: true, 35 | }); 36 | return { 37 | user: newUser, 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/auth/services/password-auth.service.ts: -------------------------------------------------------------------------------- 1 | import { MailService } from './../../mail/mail.service'; 2 | import { ForgotPasswordToken } from './../entities/forgot-password-token.entity'; 3 | import { User } from 'src/user/user.entity'; 4 | import { UserService } from './../../user/user.service'; 5 | import { 6 | BadRequestException, 7 | Injectable, 8 | NotFoundException, 9 | UnauthorizedException, 10 | } from '@nestjs/common'; 11 | import * as bcrypt from 'bcryptjs'; 12 | import { InjectRepository } from '@nestjs/typeorm'; 13 | import { Repository } from 'typeorm'; 14 | import * as jwt from 'jsonwebtoken'; 15 | 16 | @Injectable() 17 | export class PasswordService { 18 | constructor( 19 | private readonly userService: UserService, 20 | private readonly mailService: MailService, 21 | @InjectRepository(ForgotPasswordToken) 22 | private forgotPasswordRepository: Repository, 23 | ) {} 24 | 25 | /** 26 | * Update the password of a user. 27 | * 28 | * @param {User} user 29 | * The user object. 30 | * @param oldPassword 31 | * The old password of the user in plain text. 32 | * @param newPassword 33 | * The new password in plain text. 34 | * 35 | * @returns 36 | */ 37 | async changePassword( 38 | user: User, 39 | oldPassword: string, 40 | newPassword: string, 41 | ): Promise { 42 | // Probably the password is not included in the user object. Thus, we need to reload the user and include the password. 43 | if (!user.password) { 44 | user = await this.userService.find({ id: user.id }, true); 45 | } 46 | // Check if the old password is correct. 47 | const isOldPasswordCorrect: boolean = await bcrypt.compare( 48 | oldPassword, 49 | user.password, 50 | ); 51 | if (!isOldPasswordCorrect) { 52 | throw new UnauthorizedException('Old password not correct'); 53 | } 54 | 55 | // Hash new password & update entity. 56 | const password = await bcrypt.hash(newPassword, 8); 57 | return await this.userService.updateUser(user.id, { 58 | password, 59 | }); 60 | } 61 | 62 | /** 63 | * Send a reset password link to a given email that the user can then use to reset her password. 64 | * 65 | * @param {string} email 66 | * The email of the user. 67 | * 68 | * @returns 69 | */ 70 | async sendForgotPasswordLink(email: string) { 71 | const user = await this.userService.find({ email }); 72 | 73 | // For security issues we won't throw an error if there is no user with the 74 | // provided email address. 75 | if (!user) { 76 | return; 77 | } 78 | 79 | // Sign a token that will expire in 5 minutes. 80 | const token = await jwt.sign({ email }, process.env.JWT_SECRET, { 81 | expiresIn: 60 * 5, 82 | }); 83 | 84 | // Create an entry in the Forgot Password table. 85 | const forgotPasswordEntry = await this.forgotPasswordRepository.create({ 86 | email, 87 | token, 88 | }); 89 | await this.forgotPasswordRepository.save(forgotPasswordEntry); 90 | 91 | // Send email with the reset password link. 92 | const url = `${process.env.FRONTEND_URL}/auth/password/reset/${token}`; 93 | await this.mailService.sendResetPasswordLink(email, url); 94 | } 95 | 96 | /** 97 | * Let the user set a new password after declaring that she forgot it. 98 | * 99 | * @param {string} token 100 | * The token that she got per mail. Necessary for security reasons. 101 | * 102 | * @param newPassword 103 | * The new password the user wants to set. 104 | * 105 | * @returns 106 | */ 107 | async resetPassword(token: string, newPassword: string) { 108 | // Load the entry from DB with the given token. 109 | const forgotToken = await this.forgotPasswordRepository.findOne({ token }); 110 | if (!forgotToken) { 111 | throw new BadRequestException('Invalid token'); 112 | } 113 | 114 | // Decode token. Throws an error if invalid, return object with user email if valid. 115 | let emailFromToken: any; 116 | try { 117 | emailFromToken = jwt.verify(token, process.env.JWT_SECRET); 118 | } catch (error) { 119 | throw new BadRequestException('Invalid token'); 120 | } 121 | if (emailFromToken.email !== forgotToken.email) { 122 | throw new BadRequestException('Invalid token'); 123 | } 124 | 125 | const hashedPassword = await bcrypt.hash(newPassword, 8); 126 | const user = await this.userService.find({ email: forgotToken.email }); 127 | if (!user) { 128 | throw new NotFoundException('User not found'); 129 | } 130 | 131 | const updatedUser = await this.userService.updateUser(user.id, { 132 | password: hashedPassword, 133 | }); 134 | 135 | return updatedUser; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /backend/src/auth/strategies/google.strategy.ts: -------------------------------------------------------------------------------- 1 | import { OAuthService } from './../services/oauth.service'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Strategy, VerifyCallback } from 'passport-google-oauth20'; 4 | 5 | import { Injectable } from '@nestjs/common'; 6 | 7 | @Injectable() 8 | export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { 9 | constructor(private readonly OAuthService: OAuthService) { 10 | super({ 11 | clientID: process.env.GOOGLE_CLIENT_ID, 12 | clientSecret: process.env.GOOGLE_SECRET, 13 | callbackURL: process.env.GOOGLE_CALLBACK, 14 | scope: ['email', 'profile'], 15 | }); 16 | } 17 | 18 | async validate( 19 | accessToken: string, 20 | refreshToken: string, 21 | profile: any, 22 | done: VerifyCallback, 23 | ): Promise { 24 | const { name, emails, photos } = profile; 25 | 26 | const user = await this.OAuthService.socialLogin({ 27 | user: { 28 | email: emails[0].value, 29 | name: `${name.givenName} ${name.familyName}`, 30 | avatar: photos[0].value, 31 | }, 32 | socialChannel: 'google', 33 | }); 34 | 35 | return { ...user }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/auth/strategies/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { AuthService } from '../services/auth.service'; 2 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 3 | import { PassportStrategy } from '@nestjs/passport'; 4 | import { Strategy } from 'passport-local'; 5 | 6 | @Injectable() 7 | export class LocalStrategy extends PassportStrategy(Strategy) { 8 | constructor(private authService: AuthService) { 9 | super({ usernameField: 'email' }); 10 | } 11 | 12 | async validate(username: string, password: string) { 13 | const user = await this.authService.validateUser(username, password); 14 | 15 | if (!user) { 16 | throw new UnauthorizedException(); 17 | } 18 | 19 | return user; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/auth/strategies/twitter.strategy.ts: -------------------------------------------------------------------------------- 1 | import { OAuthService } from './../services/oauth.service'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Strategy, VerifyCallback } from 'passport-twitter'; 4 | import { Injectable } from '@nestjs/common'; 5 | 6 | @Injectable() 7 | export class TwitterStrategy extends PassportStrategy(Strategy, 'twitter') { 8 | constructor(private readonly oAuthService: OAuthService) { 9 | super({ 10 | consumerKey: process.env.TWITTER_CLIENT_ID, 11 | consumerSecret: process.env.TWITTER_SECRET, 12 | callbackURL: process.env.TWITTER_CALLBACK, 13 | userProfileURL: 14 | 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true', 15 | scope: ['email', 'profile'], 16 | }); 17 | } 18 | 19 | async validate( 20 | accessToken: string, 21 | refreshToken: string, 22 | profile: any, 23 | done: VerifyCallback, 24 | ): Promise { 25 | const { displayName, emails, photos } = profile; 26 | const user = await this.oAuthService.socialLogin({ 27 | user: { 28 | email: emails[0].value, 29 | name: displayName, 30 | avatar: photos[0].value, 31 | }, 32 | socialChannel: 'twitter', 33 | }); 34 | 35 | return { ...user }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { ForgotPasswordToken } from 'src/auth/entities/forgot-password-token.entity'; 4 | import { TypeORMSession } from 'src/auth/entities/session.entity'; 5 | import { User } from 'src/user/user.entity'; 6 | 7 | @Module({ 8 | imports: [ 9 | TypeOrmModule.forRoot({ 10 | type: 'postgres', 11 | host: process.env.DB_HOST || 'database', 12 | port: parseInt(process.env.DB_PORT) || 5432, 13 | username: process.env.DB_USERNAME || 'nestjs', 14 | password: process.env.DB_PASSWORD || 'nestjs', 15 | database: process.env.DB_DATABASE || 'vuenestboilerplate', 16 | entities: [User, ForgotPasswordToken, TypeORMSession], 17 | synchronize: true, 18 | }), 19 | ], 20 | }) 21 | export class DatabaseModule {} 22 | -------------------------------------------------------------------------------- /backend/src/logger/logger.module.ts: -------------------------------------------------------------------------------- 1 | import { WinstonModule } from 'nest-winston'; 2 | import { Module } from '@nestjs/common'; 3 | import * as winston from 'winston'; 4 | 5 | @Module({ 6 | imports: [ 7 | WinstonModule.forRoot({ 8 | level: 'error', 9 | transports: [ 10 | new winston.transports.File({ 11 | filename: process.env.ERROR_LOG_FILE_PATH || 'error.log', 12 | }), 13 | ], 14 | }), 15 | ], 16 | }) 17 | export class LoggerModule {} 18 | -------------------------------------------------------------------------------- /backend/src/mail/mail.module.ts: -------------------------------------------------------------------------------- 1 | import { MailerModule } from '@nestjs-modules/mailer'; 2 | import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; 3 | import { Module } from '@nestjs/common'; 4 | import { MailService } from './mail.service'; 5 | import { join } from 'path'; 6 | import { ConfigModule } from '@nestjs/config'; 7 | 8 | @Module({ 9 | imports: [ 10 | ConfigModule.forRoot(), 11 | MailerModule.forRoot({ 12 | transport: { 13 | host: process.env.MAIL_HOST, 14 | secure: false, 15 | port: process.env.MAIL_PORT, 16 | auth: { 17 | user: process.env.MAIL_USERNAME, 18 | pass: process.env.MAIL_PASSWORD, 19 | }, 20 | }, 21 | defaults: { 22 | from: process.env.MAIL_FROM_ADDRESS, 23 | }, 24 | template: { 25 | dir: join(__dirname, 'templates'), 26 | adapter: new HandlebarsAdapter(), 27 | options: { 28 | strict: true, 29 | }, 30 | }, 31 | }), 32 | ], 33 | providers: [MailService], 34 | exports: [MailService], 35 | }) 36 | export class MailModule {} 37 | -------------------------------------------------------------------------------- /backend/src/mail/mail.service.ts: -------------------------------------------------------------------------------- 1 | import { MailerService } from '@nestjs-modules/mailer'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { User } from './../user/user.entity'; 4 | 5 | @Injectable() 6 | export class MailService { 7 | constructor(private mailerService: MailerService) {} 8 | 9 | async sendUserConfirmation(user: User, app_name: string, url: string) { 10 | await this.mailerService.sendMail({ 11 | to: user.email, 12 | subject: 'Welcome to Nice App! Confirm your Email', 13 | template: './confirmation', 14 | context: { 15 | name: user.name, 16 | url, 17 | app_name, 18 | }, 19 | }); 20 | } 21 | 22 | async sendResetPasswordLink(email: string, url: string) { 23 | await this.mailerService.sendMail({ 24 | to: email, 25 | subject: 'Reset password', 26 | template: './reset_password', 27 | context: { 28 | name: email, 29 | url, 30 | app_name: 'wegweg', 31 | }, 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/src/mail/templates/confirmation.hbs: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 24 | 33 | 34 | 35 | 207 | 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /backend/src/mail/templates/reset_password.hbs: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 24 | 33 | 34 | 35 | 207 | 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /backend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { TypeORMSession } from './auth/entities/session.entity'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { NestFactory } from '@nestjs/core'; 4 | import { AppModule } from './app.module'; 5 | import * as session from 'express-session'; 6 | import { TypeormStore } from 'connect-typeorm/out'; 7 | import { getRepository } from 'typeorm'; 8 | import * as passport from 'passport'; 9 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; 10 | 11 | async function bootstrap() { 12 | const app = await NestFactory.create(AppModule); 13 | const configService = app.get(ConfigService); 14 | 15 | // Get some config from the .env file. 16 | const APP_PORT = configService.get('API_PORT') || 3000; 17 | const ALLOWED_CORS_ORIGIN = configService.get('FRONTEND_URL'); 18 | const SESSION_SECRET = 19 | configService.get('SESSION_SECRET') || 'my-not-so-secure-secret'; 20 | const ENABLE_LOGGING = configService.get('ENABLE_LOGGING') || false; 21 | 22 | // Enable sessions for authentication purposes. 23 | // Sessions are stored in the database. 24 | app.use( 25 | session({ 26 | secret: SESSION_SECRET, 27 | resave: false, 28 | saveUninitialized: false, 29 | store: new TypeormStore().connect(getRepository(TypeORMSession)), 30 | cookie: { 31 | sameSite: false, 32 | httpOnly: true, 33 | maxAge: 600000, 34 | }, 35 | }), 36 | ); 37 | 38 | // Logging. Disabled by default. 39 | if (ENABLE_LOGGING) { 40 | app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)); 41 | } 42 | 43 | // Initliaze passport & passport session support. 44 | app.use(passport.initialize()); 45 | app.use(passport.session()); 46 | 47 | // Allow requests from frontend. 48 | app.enableCors({ 49 | origin: ALLOWED_CORS_ORIGIN, 50 | credentials: true, 51 | methods: 'GET,HEAD,OPTIONS,POST,PUT,PATCH,DELETE', 52 | }); 53 | 54 | await app.listen(APP_PORT); 55 | } 56 | bootstrap(); 57 | -------------------------------------------------------------------------------- /backend/src/user/interfaces/UserInterface.ts: -------------------------------------------------------------------------------- 1 | export interface UserInterface { 2 | name: string; 3 | email: string; 4 | password?: string; 5 | isActive?: boolean; 6 | email_verified?: boolean; 7 | social_channel?: string; 8 | avatar?: string; 9 | created_at?: Date; 10 | updated_at?: Date; 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/user/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | } from 'typeorm'; 8 | 9 | @Entity() 10 | export class User { 11 | @PrimaryGeneratedColumn() 12 | id: number; 13 | 14 | @Column() 15 | name: string; 16 | 17 | @Column() 18 | email: string; 19 | 20 | @Column() 21 | password?: string; 22 | 23 | @Column({ default: true }) 24 | isActive: boolean; 25 | 26 | @Column({ default: false }) 27 | email_verified: boolean; 28 | 29 | @Column({ nullable: true }) 30 | social_channel: string; 31 | 32 | @Column({ nullable: true }) 33 | avatar: string; 34 | 35 | @CreateDateColumn() 36 | created_at: Date; 37 | 38 | @UpdateDateColumn() 39 | updated_at: Date; 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { ForgotPasswordToken } from '../auth/entities/forgot-password-token.entity'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Module } from '@nestjs/common'; 4 | import { UserService } from './user.service'; 5 | import { User } from './user.entity'; 6 | 7 | @Module({ 8 | providers: [UserService], 9 | exports: [UserService], 10 | imports: [TypeOrmModule.forFeature([User, ForgotPasswordToken])], 11 | }) 12 | export class UserModule {} 13 | -------------------------------------------------------------------------------- /backend/src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { UserInterface } from './interfaces/UserInterface'; 2 | import { 3 | BadRequestException, 4 | Injectable, 5 | InternalServerErrorException, 6 | } from '@nestjs/common'; 7 | import { InjectRepository } from '@nestjs/typeorm'; 8 | import { Repository } from 'typeorm'; 9 | import { User } from './user.entity'; 10 | import * as gravatar from 'gravatar'; 11 | 12 | @Injectable() 13 | export class UserService { 14 | constructor( 15 | @InjectRepository(User) 16 | private userRepository: Repository, 17 | ) {} 18 | 19 | async findById(id: string | number): Promise { 20 | return this.userRepository.findOne(id); 21 | } 22 | 23 | async find(options: any, withPassword = false): Promise | null { 24 | const user = await this.userRepository.findOne(options); 25 | if (!user) { 26 | return; 27 | } 28 | if (!withPassword) { 29 | delete user.password; 30 | } 31 | return user; 32 | } 33 | 34 | async createUser(user: UserInterface): Promise { 35 | const { name, email, password, avatar, social_channel, email_verified } = 36 | user; 37 | 38 | // Create default avatar. 39 | const userAvatar = 40 | avatar || gravatar.url(email, { s: '100', r: 'x', d: 'retro' }, true); 41 | 42 | const newUser = await this.userRepository.create({ 43 | name, 44 | email, 45 | password: password || '', 46 | avatar: userAvatar || '', 47 | social_channel: social_channel || '', 48 | email_verified: email_verified || false, 49 | }); 50 | const result = await this.userRepository.save(newUser); 51 | return result; 52 | } 53 | 54 | async updateUser(id: string | number, properties: any) { 55 | const user = await this.findById(id); 56 | console.log(properties); 57 | 58 | if (!user) { 59 | throw new BadRequestException('User not found'); 60 | } 61 | 62 | try { 63 | const updatedUser = await this.userRepository.save({ 64 | ...user, 65 | ...properties, 66 | }); 67 | return updatedUser; 68 | } catch (error) { 69 | throw new InternalServerErrorException(error); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /backend/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | # API 4 | api: 5 | build: . 6 | volumes: 7 | - './backend/:/var/www/html' 8 | command: bash -c "npm install && npm run start:debug" 9 | ports: 10 | - 3000:3000 11 | - 9229:9229 12 | restart: unless-stopped 13 | depends_on: 14 | - 'database' 15 | networks: 16 | - app 17 | 18 | # FRONTEND 19 | frontend: 20 | image: node:16-alpine 21 | working_dir: /var/www/html 22 | command: npm run serve 23 | ports: 24 | - 8080:8080 25 | restart: unless-stopped 26 | volumes: 27 | - './frontend/:/var/www/html' 28 | networks: 29 | - app 30 | 31 | # DATABASE 32 | database: 33 | container_name: pg_container 34 | image: postgres 35 | restart: unless-stopped 36 | environment: 37 | POSTGRES_USER: '${DB_USERNAME}' 38 | POSTGRES_PASSWORD: '${DB_PASSWORD}' 39 | POSTGRES_DB: '${DB_DATABASE}' 40 | PGDATA: /var/lib/postgresql/data 41 | volumes: 42 | - postgres-data:/var/lib/postgresql/data 43 | ports: 44 | - '${DB_PORT}:5432' 45 | networks: 46 | - app 47 | 48 | # Redis 49 | redis: 50 | image: redis:latest 51 | ports: 52 | - '6379:6379' 53 | networks: 54 | - app 55 | # rcli: 56 | # image: redis:latest 57 | # links: 58 | # - redis 59 | # command: redis-cli -h redis 60 | 61 | # Postgres admin 62 | pgadmin: 63 | container_name: pgadmin4_container 64 | image: dpage/pgadmin4 65 | restart: unless-stopped 66 | environment: 67 | PGADMIN_DEFAULT_EMAIL: admin@admin.com 68 | PGADMIN_DEFAULT_PASSWORD: root 69 | ports: 70 | - '5050:80' 71 | networks: 72 | - app 73 | 74 | # MAILHOG 75 | mailhog_app: 76 | container_name: mailhog_app 77 | image: mailhog/mailhog 78 | logging: 79 | driver: 'none' 80 | ports: 81 | - 1025:1025 82 | - 8025:8025 83 | networks: 84 | - app 85 | 86 | # NETWORKS 87 | networks: 88 | app: 89 | driver: bridge 90 | 91 | # VOLUMES 92 | volumes: 93 | postgres-data: 94 | driver: local 95 | -------------------------------------------------------------------------------- /frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /frontend/.env.local.example: -------------------------------------------------------------------------------- 1 | APP_NAME="VUE Boilerplate" 2 | VUE_APP_API_URL=http://localhost:3000 3 | VUE_APP_FRONTEND_URL=http://localhost:3000 -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "idea-selector", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "@tailwindcss/postcss7-compat": "^2.2.17", 11 | "@tailwindcss/typography": "0.5.0-alpha.2", 12 | "autoprefixer": "^9", 13 | "axios": "^0.24.0", 14 | "core-js": "^3.6.5", 15 | "postcss": "^7", 16 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.17", 17 | "vue": "^3.0.0", 18 | "vue-axios": "^3.4.0", 19 | "vue-router": "^4.0.0-0", 20 | "vuex": "^4.0.0-0" 21 | }, 22 | "devDependencies": { 23 | "@vue/cli-plugin-babel": "~4.5.0", 24 | "@vue/cli-plugin-router": "~4.5.0", 25 | "@vue/cli-plugin-vuex": "~4.5.0", 26 | "@vue/cli-service": "~4.5.0", 27 | "@vue/compiler-sfc": "^3.0.0", 28 | "node-sass": "^4.12.0", 29 | "sass-loader": "^8.0.2", 30 | "vue-cli-plugin-tailwind": "~2.2.18" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niclas-timm/nestjs-vue-boilerplate/2faed652a33dc89a2b679197d056328407ebe783/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/src/Layouts/AuthLayout.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 41 | -------------------------------------------------------------------------------- /frontend/src/Layouts/BaseLayout.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /frontend/src/Layouts/GuestLayout.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 42 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niclas-timm/nestjs-vue-boilerplate/2faed652a33dc89a2b679197d056328407ebe783/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/assets/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; 6 | -------------------------------------------------------------------------------- /frontend/src/components/Buttons/BurgerButton.vue: -------------------------------------------------------------------------------- 1 | 58 | 61 | -------------------------------------------------------------------------------- /frontend/src/components/Buttons/ButtonBase.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 37 | -------------------------------------------------------------------------------- /frontend/src/components/Buttons/PrimaryButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 23 | -------------------------------------------------------------------------------- /frontend/src/components/Buttons/Socials/GoogleAuthButton.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 40 | -------------------------------------------------------------------------------- /frontend/src/components/Buttons/Socials/TwitterAuthButton.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 39 | -------------------------------------------------------------------------------- /frontend/src/components/Form/Form.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 34 | -------------------------------------------------------------------------------- /frontend/src/components/Form/FormCard.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /frontend/src/components/Form/Input.vue: -------------------------------------------------------------------------------- 1 | 18 | 32 | -------------------------------------------------------------------------------- /frontend/src/components/Form/Select.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 36 | -------------------------------------------------------------------------------- /frontend/src/components/Form/TextArea.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 38 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/ModalBase.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 99 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/StandardModal.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 54 | -------------------------------------------------------------------------------- /frontend/src/components/Navigation/AuthenticatedNavbar.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 116 | -------------------------------------------------------------------------------- /frontend/src/components/Navigation/DefaultNavbar.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 97 | -------------------------------------------------------------------------------- /frontend/src/components/Navigation/Navbar/MobileNavbarItem.vue: -------------------------------------------------------------------------------- 1 | 11 | 16 | -------------------------------------------------------------------------------- /frontend/src/components/Navigation/Navbar/NavbarItem.vue: -------------------------------------------------------------------------------- 1 | 11 | 16 | -------------------------------------------------------------------------------- /frontend/src/components/Navigation/Navbar/ProfileMenu/ProfileIcon.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /frontend/src/components/Navigation/Navbar/ProfileMenu/ProfileMenu.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 56 | -------------------------------------------------------------------------------- /frontend/src/components/Typography/CodeSnippet.vue: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /frontend/src/components/Typography/MainHeadline.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /frontend/src/components/Typography/Paragraph.vue: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Typography/SectionHeadline.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /frontend/src/components/Typography/SmallLabel.vue: -------------------------------------------------------------------------------- 1 | 4 | 7 | -------------------------------------------------------------------------------- /frontend/src/components/Typography/SubHeader.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /frontend/src/components/Widgets/Alert.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 29 | -------------------------------------------------------------------------------- /frontend/src/components/Widgets/Badge.vue: -------------------------------------------------------------------------------- 1 | 23 | 33 | -------------------------------------------------------------------------------- /frontend/src/components/Widgets/Bar/BarElement.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /frontend/src/components/Widgets/Bar/BarWrapper.vue: -------------------------------------------------------------------------------- 1 | 14 | 17 | -------------------------------------------------------------------------------- /frontend/src/components/Widgets/Card.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /frontend/src/components/Widgets/Divider.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /frontend/src/components/Widgets/Spinner/Spinner.vue: -------------------------------------------------------------------------------- 1 | 10 | 15 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | import router from './router'; 4 | import store from './store'; 5 | import './assets/tailwind.css'; 6 | import axios from 'axios'; 7 | import VueAxios from 'vue-axios'; 8 | 9 | // Set default base url for axios. 10 | axios.defaults.baseURL = process.env.VUE_APP_API_URL; 11 | axios.defaults.withCredentials = true; 12 | 13 | createApp(App).use(store).use(router).use(VueAxios, axios).mount('#app'); 14 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router'; 2 | import Home from '../views/Home.vue'; 3 | import Dashboard from '../views/Dashboard.vue'; 4 | import Login from '../views/Auth/Login.vue'; 5 | import Register from '../views/Auth/Register.vue'; 6 | 7 | const routes = [ 8 | { 9 | path: '/', 10 | name: 'Home', 11 | component: Home, 12 | }, 13 | { 14 | path: '/dashboard', 15 | name: 'Dashboard', 16 | component: Dashboard, 17 | }, 18 | { 19 | path: '/login', 20 | name: 'Login', 21 | component: Login, 22 | }, 23 | { 24 | path: '/register', 25 | name: 'Register', 26 | component: Register, 27 | }, 28 | ]; 29 | 30 | const router = createRouter({ 31 | history: createWebHistory(process.env.BASE_URL), 32 | routes, 33 | }); 34 | 35 | export default router; 36 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex'; 2 | import AuthStore from './modules/auth'; 3 | 4 | export default createStore({ 5 | state: {}, 6 | mutations: {}, 7 | actions: {}, 8 | modules: { 9 | auth: AuthStore, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /frontend/src/store/modules/auth.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export default { 4 | //=========================== 5 | // STATEI 6 | //=========================== 7 | state: { 8 | user: {}, 9 | loadingUser: true, 10 | isAuthenticated: false, 11 | access_token: '', 12 | error: {}, 13 | }, 14 | 15 | //=========================== 16 | // Mutations. 17 | //=========================== 18 | mutations: { 19 | SET_USER(state, user) { 20 | state.user = user; 21 | state.isAuthenticated = true; 22 | state.loadingUser = false; 23 | state.error = {}; 24 | }, 25 | SET_ACCESS_TOKEN(state, token = false) { 26 | if (token) { 27 | window.localStorage.setItem('access_token', token); 28 | return; 29 | } 30 | window.localStorage.removeItem('access_token'); 31 | state.access_token = token; 32 | }, 33 | SET_USER_ERROR_OR_LOGOUT(state, error) { 34 | state.error = error; 35 | state.user = {}; 36 | state.isAuthenticated = false; 37 | state.loadingUser = false; 38 | state.access_token = ''; 39 | }, 40 | }, 41 | 42 | //=========================== 43 | // ACTIONS 44 | //=========================== 45 | actions: { 46 | /** 47 | * Fetch the currently logged in user from the DB. 48 | * @param {object} context 49 | * 50 | * @returns 51 | */ 52 | async fetchUser({ commit }) { 53 | try { 54 | // Send api request. 55 | const res = await axios.get( 56 | `${process.env.VUE_APP_API_URL}/auth/user`, 57 | { 58 | withCredentials: true, 59 | }, 60 | ); 61 | // Put user into store. 62 | if (res.status === 200) { 63 | commit('SET_USER', res.data); 64 | commit( 65 | 'SET_ACCESS_TOKEN', 66 | window.localStorage.getItem('access_token'), 67 | ); 68 | return true; 69 | } 70 | } catch (error) { 71 | if (error.response.data) { 72 | commit('SET_USER_ERROR_OR_LOGOUT', { ...error.response.data, area: 'fetchUser' }); 73 | return false; 74 | } 75 | commit('SET_USER_ERROR_OR_LOGOUT', { 76 | error: 'Server error', 77 | message: 'Sorry, something went wrong', 78 | statusCode: 500, 79 | area: 'fetchUser', 80 | }) 81 | 82 | } 83 | }, 84 | 85 | /** 86 | * Register user via email and password. 87 | * 88 | * @param {object} context 89 | * The context object. 90 | * 91 | * @param {object} formData 92 | * The form data. 93 | * @returns 94 | * 95 | */ 96 | async register({ commit }, formData) { 97 | try { 98 | const res = await axios.post( 99 | `${process.env.VUE_APP_API_URL}/auth/register`, 100 | { 101 | name: formData.name, 102 | email: formData.email, 103 | password: formData.password, 104 | }, 105 | { 106 | withCredentials: true, 107 | }, 108 | ); 109 | 110 | if (res.status === 201) { 111 | window.location = `${process.env.VUE_APP_FRONTEND_URL}/dashboard`; 112 | } 113 | } catch (error) { 114 | if (error.response.data) { 115 | commit('SET_USER_ERROR_OR_LOGOUT', { 116 | ...error.response.data, 117 | area: 'register', 118 | }); 119 | return this.error; 120 | } 121 | commit('SET_USER_ERROR_OR_LOGOUT', { 122 | error: 'Server error', 123 | message: 'Sorry, something went wrong', 124 | statusCode: 500, 125 | area: 'register', 126 | }); 127 | return this.error; 128 | } 129 | }, 130 | 131 | /** 132 | * Log in user via email and password. 133 | * 134 | * @param {object} context 135 | * The context object. 136 | * 137 | * @param {object} formData 138 | * The form data that holds email and password. 139 | * 140 | * @returns 141 | */ 142 | async login({ commit }, { email, password }) { 143 | try { 144 | const res = await axios.post( 145 | `${process.env.VUE_APP_API_URL}/auth/login`, 146 | { email, password }, 147 | { 148 | withCredentials: true, 149 | }, 150 | ); 151 | if (res.status === 201) { 152 | window.location = `${process.env.VUE_APP_FRONTEND_URL}/dashboard`; 153 | } 154 | } catch (error) { 155 | console.log(error.response) 156 | if (error.response && error.response.data) { 157 | commit('SET_USER_ERROR_OR_LOGOUT', { 158 | ...error.response.data, 159 | area: 'login', 160 | }); 161 | return this.error; 162 | } 163 | commit('SET_USER_ERROR_OR_LOGOUT', { 164 | error: 'Server error', 165 | message: 'Sorry, something went wrong', 166 | statusCode: 500, 167 | area: 'login', 168 | }); 169 | return this.error; 170 | } 171 | }, 172 | 173 | /** 174 | * Log user out. 175 | * 176 | * @param {object} context 177 | * The context object. 178 | */ 179 | async logout({ commit }) { 180 | const res = await axios.get('http://localhost:3000/auth/logout', { 181 | withCredentials: true, 182 | }); 183 | 184 | commit('SET_USER_ERROR_OR_LOGOUT'); 185 | }, 186 | }, 187 | }; 188 | -------------------------------------------------------------------------------- /frontend/src/views/Auth/Login.vue: -------------------------------------------------------------------------------- 1 | 97 | 98 | 216 | -------------------------------------------------------------------------------- /frontend/src/views/Auth/Register.vue: -------------------------------------------------------------------------------- 1 | 111 | 112 | 239 | -------------------------------------------------------------------------------- /frontend/src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 11 | 23 | -------------------------------------------------------------------------------- /frontend/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 28 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | purge: ['./public/**/*.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 4 | darkMode: false, 5 | theme: { 6 | extend: { 7 | colors: { 8 | primary: '#1f2937', 9 | accent: '#033a87', 10 | contrast: '#396cb3', 11 | secondary: '', 12 | success: { background: '#A7F3D0', color: '#10B981' }, 13 | info: { background: '#BFDBFE', color: '#3B82F6' }, 14 | danger: { background: '#FECACA', color: '#EF4444' }, 15 | }, 16 | }, 17 | }, 18 | variants: { 19 | extend: {}, 20 | }, 21 | plugins: [require('@tailwindcss/typography')], 22 | }; 23 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "@types/body-parser": { 6 | "version": "1.19.2", 7 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", 8 | "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", 9 | "dev": true, 10 | "requires": { 11 | "@types/connect": "*", 12 | "@types/node": "*" 13 | } 14 | }, 15 | "@types/connect": { 16 | "version": "3.4.35", 17 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", 18 | "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", 19 | "dev": true, 20 | "requires": { 21 | "@types/node": "*" 22 | } 23 | }, 24 | "@types/express": { 25 | "version": "4.17.13", 26 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", 27 | "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", 28 | "dev": true, 29 | "requires": { 30 | "@types/body-parser": "*", 31 | "@types/express-serve-static-core": "^4.17.18", 32 | "@types/qs": "*", 33 | "@types/serve-static": "*" 34 | } 35 | }, 36 | "@types/express-serve-static-core": { 37 | "version": "4.17.27", 38 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.27.tgz", 39 | "integrity": "sha512-e/sVallzUTPdyOTiqi8O8pMdBBphscvI6E4JYaKlja4Lm+zh7UFSSdW5VMkRbhDtmrONqOUHOXRguPsDckzxNA==", 40 | "dev": true, 41 | "requires": { 42 | "@types/node": "*", 43 | "@types/qs": "*", 44 | "@types/range-parser": "*" 45 | } 46 | }, 47 | "@types/mime": { 48 | "version": "1.3.2", 49 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", 50 | "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", 51 | "dev": true 52 | }, 53 | "@types/node": { 54 | "version": "17.0.5", 55 | "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.5.tgz", 56 | "integrity": "sha512-w3mrvNXLeDYV1GKTZorGJQivK6XLCoGwpnyJFbJVK/aTBQUxOCaa/GlFAAN3OTDFcb7h5tiFG+YXCO2By+riZw==", 57 | "dev": true 58 | }, 59 | "@types/oauth": { 60 | "version": "0.9.1", 61 | "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.1.tgz", 62 | "integrity": "sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==", 63 | "dev": true, 64 | "requires": { 65 | "@types/node": "*" 66 | } 67 | }, 68 | "@types/passport": { 69 | "version": "1.0.7", 70 | "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.7.tgz", 71 | "integrity": "sha512-JtswU8N3kxBYgo+n9of7C97YQBT+AYPP2aBfNGTzABqPAZnK/WOAaKfh3XesUYMZRrXFuoPc2Hv0/G/nQFveHw==", 72 | "dev": true, 73 | "requires": { 74 | "@types/express": "*" 75 | } 76 | }, 77 | "@types/passport-google-oauth20": { 78 | "version": "2.0.11", 79 | "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.11.tgz", 80 | "integrity": "sha512-9XMT1GfwhZL7UQEiCepLef55RNPHkbrCtsU7rsWPTEOsmu5qVIW8nSemtB4p+P24CuOhA+IKkv8LsPThYghGww==", 81 | "dev": true, 82 | "requires": { 83 | "@types/express": "*", 84 | "@types/passport": "*", 85 | "@types/passport-oauth2": "*" 86 | } 87 | }, 88 | "@types/passport-oauth2": { 89 | "version": "1.4.11", 90 | "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.11.tgz", 91 | "integrity": "sha512-KUNwmGhe/3xPbjkzkPwwcPmyFwfyiSgtV1qOrPBLaU4i4q9GSCdAOyCbkFG0gUxAyEmYwqo9OAF/rjPjJ6ImdA==", 92 | "dev": true, 93 | "requires": { 94 | "@types/express": "*", 95 | "@types/oauth": "*", 96 | "@types/passport": "*" 97 | } 98 | }, 99 | "@types/qs": { 100 | "version": "6.9.7", 101 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", 102 | "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", 103 | "dev": true 104 | }, 105 | "@types/range-parser": { 106 | "version": "1.2.4", 107 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", 108 | "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", 109 | "dev": true 110 | }, 111 | "@types/serve-static": { 112 | "version": "1.13.10", 113 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", 114 | "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", 115 | "dev": true, 116 | "requires": { 117 | "@types/mime": "^1", 118 | "@types/node": "*" 119 | } 120 | }, 121 | "base64url": { 122 | "version": "3.0.1", 123 | "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", 124 | "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" 125 | }, 126 | "oauth": { 127 | "version": "0.9.15", 128 | "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", 129 | "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" 130 | }, 131 | "passport-google-oauth20": { 132 | "version": "2.0.0", 133 | "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", 134 | "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", 135 | "requires": { 136 | "passport-oauth2": "1.x.x" 137 | } 138 | }, 139 | "passport-oauth2": { 140 | "version": "1.6.1", 141 | "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.6.1.tgz", 142 | "integrity": "sha512-ZbV43Hq9d/SBSYQ22GOiglFsjsD1YY/qdiptA+8ej+9C1dL1TVB+mBE5kDH/D4AJo50+2i8f4bx0vg4/yDDZCQ==", 143 | "requires": { 144 | "base64url": "3.x.x", 145 | "oauth": "0.9.x", 146 | "passport-strategy": "1.x.x", 147 | "uid2": "0.0.x", 148 | "utils-merge": "1.x.x" 149 | } 150 | }, 151 | "passport-strategy": { 152 | "version": "1.0.0", 153 | "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", 154 | "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" 155 | }, 156 | "uid2": { 157 | "version": "0.0.4", 158 | "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", 159 | "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" 160 | }, 161 | "utils-merge": { 162 | "version": "1.0.1", 163 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 164 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 165 | } 166 | } 167 | } 168 | --------------------------------------------------------------------------------