├── .env ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── environment.d.ts ├── nest-cli.json ├── package.json ├── src ├── app.module.ts ├── auth │ ├── auth.module.ts │ ├── controllers │ │ └── auth │ │ │ ├── auth.controller.spec.ts │ │ │ └── auth.controller.ts │ ├── services │ │ └── auth │ │ │ ├── auth.service.spec.ts │ │ │ ├── auth.service.ts │ │ │ └── auth.ts │ └── utils │ │ ├── DiscordStrategy.ts │ │ ├── Guards.ts │ │ └── Serializer.ts ├── discord │ ├── discord.module.ts │ ├── discord.service.spec.ts │ ├── discord.service.ts │ └── discord.ts ├── graphql │ ├── index.graphql │ ├── index.ts │ └── resolvers │ │ └── User.resolver.ts ├── main.ts ├── typeorm │ ├── entities │ │ ├── Session.ts │ │ └── User.ts │ └── index.ts └── utils │ └── types.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | # Database Credentials 2 | 3 | MYSQL_DB_HOST= 4 | MYSQL_DB_PORT= 5 | MYSQL_DB_USER= 6 | MYSQL_DB_PASS= 7 | MYSQL_DB_NAME= 8 | 9 | # Server Port 10 | 11 | PORT= 12 | 13 | # Discord Credentials 14 | 15 | DISCORD_CLIENT_ID= 16 | DISCORD_CLIENT_SECRET= 17 | DISCORD_CALLBACK_URL= 18 | -------------------------------------------------------------------------------- /.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 | 'prettier/@typescript-eslint', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | # Environment Files 37 | .env.development 38 | .env.production 39 | .env.testing -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nest.JS API w/ TypeORM, GraphQL, Discord OAuth2 2 | 3 | This is a base template that we will use for the Discord Dashboard Tutorial. 4 | 5 | # Installation & Setup 6 | 7 | 1. Clone this repository 8 | 2. Run `npm i` or `yarn install` to install all dependencies 9 | 3. Install MySQL using Docker or natively on your computer 10 | 4. Configure your environment variables 11 | 5. Run `yarn start:dev` 12 | 13 | # Project Details 14 | 15 | This project uses TypeORM & MySQL. You will need to install MySQL on your computer or use Docker. You can also easily swap the Database to PostgreSQL or any supported database driver from TypeORM. 16 | 17 | I've left a basic `.env` file in this repository with all of the environment variables used. They're pretty self explanatory but I will walk you through each one: 18 | 19 | **MySQL Environment Variables** 20 | 21 | These are all of the environment variables you'll need to connect to your MySQL database. 22 | 23 | ``` 24 | MYSQL_DB_HOST= 25 | MYSQL_DB_PORT= 26 | MYSQL_DB_USER= 27 | MYSQL_DB_PASS= 28 | MYSQL_DB_NAME= 29 | ``` 30 | 31 | `MYSQL_DB_HOST` is the hostname of the server of MySQL. Typically this will be `localhost` unless you connect to a remote database. 32 | 33 | `MYSQL_DB_PORT` is the Port that the MySQL server listens for TCP connections on. 34 | 35 | `MYSQL_DB_USER` is the username to use to connect to the MySQL server (if you have authentication enabled) 36 | 37 | `MYSQL_DB_PASS` is the password for the MySQL server 38 | 39 | `MYSQL_DB_NAME` is the name of the database. You will need to create it since TypeORM will not create it for you. 40 | 41 | **Server Environment Variables** 42 | 43 | `PORT` is the port the Nest.JS Server will listen to requests on. 44 | 45 | **Discord OAuth2 Environment Variables** 46 | 47 | `DISCORD_CLIENT_ID` The Client ID of the Discord Application 48 | 49 | `DISCORD_CLIENT_SECRET` The client secret of the Discord Application (THIS IS SENSITIVE INFO) 50 | 51 | `DISCORD_CALLBACK_URL` The callback URL upon successful authorization from Discord. 52 | 53 | _You'll need to go over to https://discord.com/developers/applications to create an application and get the credentials_ 54 | 55 | # Handling CORS 56 | 57 | Read about CORS [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) 58 | 59 | By default, React applications generated with Create React App run on Port 3000. So in this application, we have handled CORS to allow origins from localhost:3000. If you have your app running on a different Port, you'll need to change the cors configuration inside `src/main.ts` and `src/app.module.ts` 60 | 61 | We also have `credentials: true` set to ensure we are handling cookies/credentials sent from the client. 62 | 63 | ```TS 64 | async function bootstrap() { 65 | const app = await NestFactory.create(AppModule); 66 | const PORT = process.env.PORT || 3003; 67 | app.setGlobalPrefix('api'); 68 | app.enableCors({ 69 | origin: ['http://localhost:3000'], 70 | credentials: true, 71 | }); 72 | await app.listen(PORT, () => console.log(`Running on Port ${PORT}`)); 73 | } 74 | bootstrap(); 75 | ``` 76 | 77 | Read more about [Access-Control-Allow-Credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) 78 | 79 | _Note: When using GraphQL Playground, make sure you click on "Settings" and set request.credentials to include_ 80 | 81 | # Getting Discord Credentials 82 | 83 | Visit https://discord.com/developers/applications and click "Create Application" 84 | 85 | Give your application a name. Afterwards, you will see "CLIENT ID" and "CLIENT SECRET" on the page. Copy both of them and save it to the .env file in the correponding environment variable. 86 | 87 | ``` 88 | DISCORD_CLIENT_ID=123 89 | DISCORD_CLIENT_SECRET=1234567891013324 90 | ``` 91 | 92 | You will need to go to the "OAuth2" tab and then add a redirect URI. A Redirect URI is for the OAuth2 Provider to call the endpoint with a `code` upon successful authorization. The `code` is used by your authentication method (in our case it would be Passport.js), to exchange it with the provider (Discord) for a pair of access & refresh tokens, user details, or anything that we are given based on `scopes` we provide. 93 | 94 | In this project, in `src/auth/controllers/auth/auth.controller.ts`, we have created a route to handle the redirect URI. 95 | 96 | This route is `/api/auth/redirect`, the full path is `http://{hostname}/api/auth/redirect` 97 | 98 | ```TS 99 | @Get('redirect') 100 | @UseGuards(DiscordAuthGuard) 101 | redirect(@Res() res: Response) { 102 | res.send(200); 103 | } 104 | ``` 105 | 106 | Make sure you add the correct redirect URI on Discord's OAuth2 Tab: 107 | 108 | ![img](https://i.imgur.com/d5GXJ5r.png) 109 | 110 | If you add the wrong redirect URI, you will see an "invalid redirect uri" error. 111 | 112 | # Authentication with Passport 113 | 114 | ## DiscordStrategy 115 | 116 | In our `src/auth/utils/DiscordStrategy.ts` file, we have created the `DiscordStrategy` class. This class extends `PassportStrategy`. We pass in `Strategy`, which is imported from `passport-discord` so that Nest knows what provider we're working with. 117 | 118 | ```TS 119 | import { Profile, Strategy } from 'passport-discord'; 120 | import { PassportStrategy } from '@nestjs/passport'; 121 | import { Inject, Injectable } from '@nestjs/common'; 122 | import { AuthenticationProvider } from '../services/auth/auth'; 123 | 124 | @Injectable() 125 | export class DiscordStrategy extends PassportStrategy(Strategy) { 126 | constructor( 127 | @Inject('AUTH_SERVICE') 128 | private readonly authService: AuthenticationProvider, 129 | ) { 130 | super({ 131 | clientID: process.env.DISCORD_CLIENT_ID, 132 | clientSecret: process.env.DISCORD_CLIENT_SECRET, 133 | callbackURL: process.env.DISCORD_CALLBACK_URL, 134 | scope: ['identify', 'guilds'], 135 | }); 136 | } 137 | 138 | async validate(accessToken: string, refreshToken: string, profile: Profile) { 139 | const { username, discriminator, id: discordId, avatar } = profile; 140 | const details = { username, discriminator, discordId, avatar }; 141 | return this.authService.validateUser(details); 142 | } 143 | } 144 | ``` 145 | 146 | We only have the `scope` set to provide the user's details and their guilds, if you want their email, you'll need to add the `email` scope in the array. 147 | 148 | If you want more scopes, you can visit the OAuth2 tab in the application portal on Discord. Here's a quick screenshot of all current scopes as of the time writing this, 12/24/2020 149 | 150 | ![img](https://i.imgur.com/ZsmZ3Iz.png) 151 | 152 | Our `DiscordStrategy` is a provider that will be used by Nest. We invoke the strategy by using NestJS' `AuthGuards`. 153 | 154 | ## DiscordAuthGuard 155 | 156 | In `src/auth/utils/Guards.ts` we have: 157 | 158 | ```TS 159 | @Injectable() 160 | export class DiscordAuthGuard extends AuthGuard('discord') { 161 | async canActivate(context: ExecutionContext) { 162 | const activate = (await super.canActivate(context)) as boolean; 163 | const request = context.switchToHttp().getRequest(); 164 | await super.logIn(request); 165 | return activate; 166 | } 167 | } 168 | ``` 169 | 170 | This Guard extends the `AuthGuard` class, passing in the string `discord` which is the name of our strategy. The nice thing about Passport is we can easily swap strategies. If you want to use Google or Github you would just change the name to `google` or `github`. 171 | 172 | We must invoke `DiscordAuthGuard` in our two route handlers in `AuthController`. 173 | 174 | ```TS 175 | @Get('login') 176 | @UseGuards(DiscordAuthGuard) 177 | login() { 178 | return; 179 | } 180 | 181 | @Get('redirect') 182 | @UseGuards(DiscordAuthGuard) 183 | redirect(@Res() res: Response) { 184 | res.send(200); 185 | } 186 | ``` 187 | 188 | One is the main route we visit to authenticate users, the other is the redirect route for the provider to send us the `code` as a query parameter. 189 | 190 | ## Saving User to Database upon Authentication 191 | 192 | We save the user to the database upon their first time authenticating with our OAuth2 provider. Remember that the Discord Strategy will be invoked internally. It will call the `validate` function: 193 | 194 | ```TS 195 | async validate(accessToken: string, refreshToken: string, profile: Profile) { 196 | const { username, discriminator, id: discordId, avatar } = profile; 197 | const details = { username, discriminator, discordId, avatar }; 198 | return this.authService.validateUser(details); 199 | } 200 | ``` 201 | 202 | This is where we receive our access token, refresh token, and profile. `Profile` will give us the users details such as username, unique id, avatar, etc. 203 | 204 | Our main responsibility in this function is to return a `User` instance. Unlike Passport Local, we don't need to worry about checking if the user's credentials are valid, we always return a user instance because we will either return an existing User, or create a new user and return the new record. 205 | 206 | So we extract the fields we need to match our `User` entity from `src/typeorm/entities/User.ts` and we pass it along to `authServie.validateUser()` to handle our logic to keep our application within the Single Responsibility Principle. 207 | 208 | ## AuthService 209 | 210 | The `AuthService` class is a `Provider` which we can inject anywhere in our application so as long we correctly import the module it lives in. We have three methods in this class: 211 | 212 | ```TS 213 | // src/auth/services/auth.service.ts 214 | @Injectable() 215 | export class AuthService implements AuthenticationProvider { 216 | constructor(@InjectRepository(User) private userRepo: Repository) {} 217 | 218 | async validateUser(details: UserDetails) { 219 | const { discordId } = details; 220 | const user = await this.userRepo.findOne({ discordId }); 221 | if (user) return user; 222 | return this.createUser(details); 223 | } 224 | 225 | createUser(details: UserDetails) { 226 | const user = this.userRepo.create(details); 227 | return this.userRepo.save(user); 228 | } 229 | 230 | findUser(discordId: string): Promise { 231 | return this.userRepo.findOne({ discordId }); 232 | } 233 | } 234 | ``` 235 | 236 | `AuthService` implements the `AuthenticationProvider` interface. There's nothing forcing us to actually follow this pattern, but we _should_ for best practices. Following this practice ensure that we program to the _interface_ rather than _implementation_. Something for you to read up on. 237 | 238 | Putting our main focus to `validateUser`, here we reference our `userRepo` field which is a Typeorm repository for our User entity. This allows us to perform basic CRUD operations. We search for a user by `discordId`. If the user is found, we return the user. If not, we call `createUser()`. 239 | 240 | `createUser()` will simply create the user, and then save it. Note that our `UserDetails` type was structured to match our Typeorm entity so we can safely pass it in `userRepo.create()` without any type errors. 241 | 242 | ```TS 243 | type UserDetails = { 244 | username: string; 245 | discriminator: string; 246 | discordId: string; 247 | avatar: string; 248 | }; 249 | ``` 250 | 251 | Once we return from `validateUser`, we are back in `validate` in the `DiscordStrategy` class, which will return the user instance. From here, we're done with the logic of Passport & saving users to the database. However, this application is not complete. We only have users saved to the database, but we have no way of persisting the user's session, so we'll never know if they're logged in or not. In the next section, we will walk through sessions and how we handle them in this project. 252 | 253 | In the `app.module.ts` file, make sure you include `PassportModule.register({ session: true })` in the `imports` 254 | 255 | ```TS 256 | @Module({ 257 | imports: [ 258 | PassportModule.register({ session: true }), 259 | ], 260 | controllers: [], 261 | }) 262 | export class AppModule {} 263 | ``` 264 | 265 | In the `main.ts` file, make sure you register `passport` middleware. Here we import passport and then initialize it along with session. 266 | 267 | ```TS 268 | app.use(passport.initialize()); 269 | app.use(passport.session()); 270 | ``` 271 | 272 | ```TS 273 | import { NestFactory } from '@nestjs/core'; 274 | import { AppModule } from './app.module'; 275 | import * as session from 'express-session'; 276 | import * as passport from 'passport'; 277 | import { getRepository } from 'typeorm'; 278 | import { TypeORMSession } from './typeorm/entities/Session'; 279 | import { TypeormStore } from 'connect-typeorm'; 280 | 281 | async function bootstrap() { 282 | const app = await NestFactory.create(AppModule); 283 | const PORT = process.env.PORT || 3003; 284 | const sessionRepo = getRepository(TypeORMSession); 285 | app.setGlobalPrefix('api'); 286 | app.enableCors({ 287 | origin: ['http://localhost:3000'], 288 | credentials: true, 289 | }); 290 | app.use( 291 | session({ 292 | cookie: { 293 | maxAge: 86400000, 294 | }, 295 | secret: 'secret', 296 | resave: false, 297 | saveUninitialized: false, 298 | store: new TypeormStore().connect(sessionRepo), 299 | }), 300 | ); 301 | app.use(passport.initialize()); 302 | app.use(passport.session()); 303 | await app.listen(PORT, () => console.log(`Running on Port ${PORT}`)); 304 | } 305 | bootstrap(); 306 | ``` 307 | 308 | # Sessions 309 | 310 | A Session is just a unique identifier that maps to a user and the user's "session" data. Because HTTP is stateless, every request made is seen as a new request, providing us zero context from the client that's making those requests. Because many complex applications depend heavily on state, we need a way to keep track of authenticated users. 311 | 312 | We use sessions to map a unique identifier, typically a random Base64 string or a UUID, to a user. The session ID is usually generated on the backend, and sent back to the client with specific headers telling the client (Browser, Postman, etc.) to set the cookie with the given value. 313 | 314 | From there, our client will usually send the cookie by default. On the server, we can see in the headers that there is a cookie present that corresponds to the Session ID we generated. We know we generated this session ID for user xyz, so we can then perform some business logic for said user. 315 | 316 | This is just a simple overview of sessions. It's important to understand, but let's go into our `src/auth/utils/Serializer.ts` file since that's where the Session Serialization & Deserialization takes place. 317 | 318 | ```TS 319 | // src/auth/utils/Serializer.ts 320 | export type Done = (err: Error, user: User) => void; 321 | 322 | @Injectable() 323 | export class SessionSerializer extends PassportSerializer { 324 | constructor( 325 | @Inject('AUTH_SERVICE') 326 | private readonly authService: AuthenticationProvider, 327 | ) { 328 | super(); 329 | } 330 | 331 | serializeUser(user: User, done: Done) { 332 | done(null, user); 333 | } 334 | 335 | async deserializeUser(user: User, done: Done) { 336 | const userDB = await this.authService.findUser(user.discordId); 337 | return userDB ? done(null, userDB) : done(null, null); 338 | } 339 | } 340 | ``` 341 | 342 | Our main focus is the `serializeUser` and `deserializeUser` methods. `serializeUser` tells Passport how to save the session. You pass in whatever field you want into the 2nd parameter of the `done` function. Here, we pass in the user instance. We could get away with just passing in the user's discordId. You should pass in a unique value for the user since whatever you pass in will be the first parameter that `deserializeUser` expects. 343 | 344 | Since we pass a `User` type, we can expect the first parameter of `deserializeUser` to also be a `User` type. If we passed in `user.discordId`, then our `deserializeUser` method signature would look like: 345 | 346 | ```TS 347 | async deserializeUser(discordId: string, done: Done) { 348 | const userDB = await this.authService.findUser(discordId); 349 | return userDB ? done(null, userDB) : done(null, null); 350 | } 351 | ``` 352 | 353 | It's important you handle `deserializeUser` properly. Every request made to your API will go through this method. It will search for the correct user by its `discordId` and return it if it was found by calling the `done` function. If not, it returns `null`. 354 | 355 | In order for all of this to work, we need to make sure we provide our `SessionSerializer` class in the module. Since `SessionSerializer` lives in `src/auth` module, we can just provide it in the `AuthModule`, which is imported in `AppModule`, making it available in our application. 356 | 357 | ```TS 358 | // auth.module.ts 359 | @Module({ 360 | controllers: [AuthController], 361 | providers: [ 362 | DiscordStrategy, 363 | SessionSerializer, 364 | { 365 | provide: 'AUTH_SERVICE', 366 | useClass: AuthService, 367 | }, 368 | ], 369 | imports: [TypeOrmModule.forFeature([User])], 370 | exports: [ 371 | { 372 | provide: 'AUTH_SERVICE', 373 | useClass: AuthService, 374 | }, 375 | ], 376 | }) 377 | export class AuthModule {} 378 | ``` 379 | 380 | You need to also make sure you have installed `express-session` and `@types/express-session` in order for sessions to work. You should also verify you installed `passport`, `passport-discord`, `@nestjs/passport` as well. All of the dependencies for this repository have been installed, however. 381 | 382 | In the `app.module.ts` file, make sure you register the PassportModule. 383 | 384 | ```TS 385 | @Module({ 386 | imports: [ 387 | PassportModule.register({ session: true }), 388 | ], 389 | controllers: [], 390 | }) 391 | export class AppModule {} 392 | ``` 393 | 394 | Then, make sure you import `express-session` and `passport`. Let's walk through this step by step. 395 | 396 | ```TS 397 | import { NestFactory } from '@nestjs/core'; 398 | import { AppModule } from './app.module'; 399 | import * as session from 'express-session'; 400 | import * as passport from 'passport'; 401 | 402 | async function bootstrap() { 403 | const app = await NestFactory.create(AppModule); 404 | const PORT = process.env.PORT || 3003; 405 | const sessionRepo = getRepository(TypeORMSession); 406 | app.setGlobalPrefix('api'); 407 | app.enableCors({ 408 | origin: ['http://localhost:3000'], 409 | credentials: true, 410 | }); 411 | app.use( 412 | session({ 413 | cookie: { 414 | maxAge: 86400000, 415 | }, 416 | secret: 'secret', 417 | resave: false, 418 | saveUninitialized: false, 419 | store: new TypeormStore().connect(sessionRepo), 420 | }), 421 | ); 422 | app.use(passport.initialize()); 423 | app.use(passport.session()); 424 | await app.listen(PORT, () => console.log(`Running on Port ${PORT}`)); 425 | } 426 | bootstrap(); 427 | ``` 428 | 429 | Here we register our session middleware. All of the session options are documented on the official docs [here](https://www.npmjs.com/package/express-session#options). 430 | 431 | ```TS 432 | app.use( 433 | session({ 434 | cookie: { 435 | maxAge: 86400000, 436 | }, 437 | secret: 'secret', 438 | resave: false, 439 | saveUninitialized: false, 440 | store: new TypeormStore().connect(sessionRepo), 441 | }), 442 | ); 443 | ``` 444 | 445 | That's really it. The `store` property is for saving sessions to our Database. This allows us to have a persistent session for each user, in the event of our server crashing or restarting, we will have our session saved in the database which will allow the session to be restored, making it so the user does not need to re-login or re-authenticate. 446 | 447 | # More on AuthGuards 448 | 449 | ## AuthenticatedGuard 450 | 451 | ```TS 452 | @Injectable() 453 | export class AuthenticatedGuard implements CanActivate { 454 | async canActivate(context: ExecutionContext): Promise { 455 | const req = context.switchToHttp().getRequest(); 456 | return req.isAuthenticated(); 457 | } 458 | } 459 | ``` 460 | 461 | This guard is used for protecting our routes at the Controller-level to prevent certain routes from being accessed if the user is not authenticated. We have used this in `src/auth/controllers/auth/auth.controller.ts` 462 | 463 | ```TS 464 | @Get('status') 465 | @UseGuards(AuthenticatedGuard) 466 | status(@Req() req: Request) { 467 | return req.user; 468 | } 469 | ``` 470 | 471 | This will protect our `/api/auth/status` route. If the user is authenticated, it will return the user details. If not, it will return a 403 status. 472 | 473 | ## GraphQLAuthGuard 474 | 475 | This is similar to `AuthenticatedGuard` except it's for GraphQL. We can apply this to our resolvers. 476 | 477 | ```TS 478 | @Injectable() 479 | export class GraphQLAuthGuard implements CanActivate { 480 | canActivate(context: ExecutionContext) { 481 | const ctx = GqlExecutionContext.create(context); 482 | return ctx.getContext().req.user; 483 | } 484 | } 485 | ``` 486 | 487 | ```TS 488 | @Resolver('User') 489 | @UseGuards(GraphQLAuthGuard) 490 | export class UserResolver { 491 | constructor( 492 | @Inject('AUTH_SERVICE') 493 | private readonly authService: AuthenticationProvider, 494 | ) {} 495 | 496 | @Query('getUser') 497 | async getUser(@CurrentUser() user: User): Promise { 498 | console.log(user); 499 | return user; 500 | } 501 | } 502 | ``` 503 | 504 | This will protect our entire resolver. To protect only a single query, you can place the `@UseGuards(GraphQLAuthGuard)` decorator on the corresponding function. 505 | -------------------------------------------------------------------------------- /environment.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | export interface ProcessEnv { 3 | MYSQL_DB_HOST?: string; 4 | MYSQL_DB_PORT?: string; 5 | MYSQL_DB_USER?: string; 6 | MYSQL_DB_PASS?: string; 7 | MYSQL_DB_NAME?: string; 8 | PORT?: string; 9 | ENVIRONMENT: Environment; 10 | DISCORD_CLIENT_ID?: string; 11 | DISCORD_CLIENT_SECRET?: string; 12 | DISCORD_CALLBACK_URL?: string; 13 | REDIS_URI?: string; 14 | } 15 | export type Environment = 'DEVELOPMENT' | 'PRODUCTION' | 'TEST'; 16 | } 17 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dashboard-nestjs-backend", 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 --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/common": "^7.5.1", 25 | "@nestjs/config": "^0.6.1", 26 | "@nestjs/core": "^7.5.1", 27 | "@nestjs/graphql": "^7.9.1", 28 | "@nestjs/passport": "^7.1.5", 29 | "@nestjs/platform-express": "^7.5.1", 30 | "@nestjs/typeorm": "^7.1.5", 31 | "apollo-server-express": "^2.19.1", 32 | "axios": "^0.21.1", 33 | "connect-redis": "^5.0.0", 34 | "connect-typeorm": "^1.1.4", 35 | "express-session": "^1.17.1", 36 | "graphql": "^15.4.0", 37 | "graphql-tools": "^7.0.2", 38 | "mysql2": "^2.2.5", 39 | "passport": "^0.4.1", 40 | "passport-discord": "^0.1.4", 41 | "redis": "^3.0.2", 42 | "reflect-metadata": "^0.1.13", 43 | "rimraf": "^3.0.2", 44 | "rxjs": "^6.6.3", 45 | "typeorm": "^0.2.29" 46 | }, 47 | "devDependencies": { 48 | "@nestjs/cli": "^7.5.1", 49 | "@nestjs/schematics": "^7.1.3", 50 | "@nestjs/testing": "^7.5.1", 51 | "@types/connect-redis": "^0.0.16", 52 | "@types/express": "^4.17.8", 53 | "@types/express-session": "^1.17.3", 54 | "@types/jest": "^26.0.15", 55 | "@types/node": "^14.14.6", 56 | "@types/passport-discord": "^0.1.3", 57 | "@types/redis": "^2.8.28", 58 | "@types/supertest": "^2.0.10", 59 | "@typescript-eslint/eslint-plugin": "^4.6.1", 60 | "@typescript-eslint/parser": "^4.6.1", 61 | "eslint": "^7.12.1", 62 | "eslint-config-prettier": "^6.15.0", 63 | "eslint-plugin-prettier": "^3.1.4", 64 | "jest": "^26.6.3", 65 | "prettier": "^2.1.2", 66 | "supertest": "^6.0.0", 67 | "ts-jest": "^26.4.3", 68 | "ts-loader": "^8.0.8", 69 | "ts-node": "^9.0.0", 70 | "tsconfig-paths": "^3.9.0", 71 | "typescript": "^4.0.5" 72 | }, 73 | "jest": { 74 | "moduleFileExtensions": [ 75 | "js", 76 | "json", 77 | "ts" 78 | ], 79 | "rootDir": "src", 80 | "testRegex": ".*\\.spec\\.ts$", 81 | "transform": { 82 | "^.+\\.(t|j)s$": "ts-jest" 83 | }, 84 | "collectCoverageFrom": [ 85 | "**/*.(t|j)s" 86 | ], 87 | "coverageDirectory": "../coverage", 88 | "testEnvironment": "node" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule, Module } from '@nestjs/common'; 2 | import { AuthModule } from './auth/auth.module'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { ConfigModule } from '@nestjs/config'; 5 | import { entities } from './typeorm'; 6 | import { PassportModule } from '@nestjs/passport'; 7 | import { GraphQLModule } from '@nestjs/graphql'; 8 | import { join } from 'path'; 9 | import { UserResolver } from './graphql/resolvers/User.resolver'; 10 | import { DiscordModule } from './discord/discord.module'; 11 | 12 | let envFilePath = '.env.development'; 13 | 14 | console.log(`Running in ${process.env.ENVIRONMENT}`); 15 | if (process.env.ENVIRONMENT === 'PRODUCTION') { 16 | envFilePath = '.env.production'; 17 | } else if (process.env.ENVIRONMENT === 'TEST') { 18 | envFilePath = '.env.testing'; 19 | } 20 | 21 | @Module({ 22 | imports: [ 23 | ConfigModule.forRoot({ envFilePath }), 24 | AuthModule, 25 | PassportModule.register({ session: true }), 26 | TypeOrmModule.forRoot({ 27 | type: 'mysql', 28 | host: process.env.MYSQL_DB_HOST, 29 | port: Number.parseInt(process.env.MYSQL_DB_PORT), 30 | username: process.env.MYSQL_DB_USER, 31 | password: process.env.MYSQL_DB_PASS, 32 | database: process.env.MYSQL_DB_NAME, 33 | entities, 34 | synchronize: true, 35 | }), 36 | HttpModule, 37 | GraphQLModule.forRoot({ 38 | typePaths: ['./**/*.graphql'], 39 | definitions: { path: join(process.cwd(), 'src', 'graphql', 'index.ts') }, 40 | useGlobalPrefix: true, 41 | cors: { origin: 'http://localhost:3000' }, 42 | }), 43 | DiscordModule, 44 | ], 45 | controllers: [], 46 | providers: [UserResolver], 47 | }) 48 | export class AppModule {} 49 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { User } from 'src/typeorm'; 4 | import { AuthController } from './controllers/auth/auth.controller'; 5 | import { AuthService } from './services/auth/auth.service'; 6 | import { DiscordStrategy } from './utils/DiscordStrategy'; 7 | import { SessionSerializer } from './utils/Serializer'; 8 | 9 | @Module({ 10 | controllers: [AuthController], 11 | providers: [ 12 | DiscordStrategy, 13 | SessionSerializer, 14 | { 15 | provide: 'AUTH_SERVICE', 16 | useClass: AuthService, 17 | }, 18 | ], 19 | imports: [TypeOrmModule.forFeature([User])], 20 | exports: [ 21 | { 22 | provide: 'AUTH_SERVICE', 23 | useClass: AuthService, 24 | }, 25 | ], 26 | }) 27 | export class AuthModule {} 28 | -------------------------------------------------------------------------------- /src/auth/controllers/auth/auth.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthController } from './auth.controller'; 3 | 4 | describe('AuthController', () => { 5 | let controller: AuthController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [AuthController], 10 | }).compile(); 11 | 12 | controller = module.get(AuthController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/auth/controllers/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common'; 2 | import { Request, Response } from 'express'; 3 | import { AuthenticatedGuard, DiscordAuthGuard } from 'src/auth/utils/Guards'; 4 | 5 | @Controller('auth') 6 | export class AuthController { 7 | /** 8 | * GET /api/auth/login 9 | * This is the route the user will visit to authenticate 10 | */ 11 | @Get('login') 12 | @UseGuards(DiscordAuthGuard) 13 | login() { 14 | return; 15 | } 16 | 17 | /** 18 | * GET /api/auth/redirect 19 | * This is the redirect URL the OAuth2 Provider will call. 20 | */ 21 | @Get('redirect') 22 | @UseGuards(DiscordAuthGuard) 23 | redirect(@Res() res: Response) { 24 | res.redirect('http://localhost:3000/dashboard'); 25 | } 26 | 27 | /** 28 | * GET /api/auth/status 29 | * Retrieve the auth status 30 | */ 31 | @Get('status') 32 | @UseGuards(AuthenticatedGuard) 33 | status(@Req() req: Request) { 34 | return req.user; 35 | } 36 | 37 | /** 38 | * GET /api/auth/logout 39 | * Logging the user out 40 | */ 41 | @Get('logout') 42 | @UseGuards(AuthenticatedGuard) 43 | logout(@Req() req: Request) { 44 | req.logOut(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/auth/services/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthService } from './auth.service'; 3 | 4 | describe('AuthService', () => { 5 | let service: AuthService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthService], 10 | }).compile(); 11 | 12 | service = module.get(AuthService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/auth/services/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { User } from '../../../typeorm'; 5 | import { UserDetails } from '../../../utils/types'; 6 | import { AuthenticationProvider } from './auth'; 7 | 8 | @Injectable() 9 | export class AuthService implements AuthenticationProvider { 10 | constructor(@InjectRepository(User) private userRepo: Repository) {} 11 | 12 | async validateUser(details: UserDetails) { 13 | const { discordId } = details; 14 | const user = await this.userRepo.findOne({ discordId }); 15 | if (user) { 16 | await this.userRepo.update({ discordId }, details); 17 | console.log('Updated'); 18 | return user; 19 | } 20 | return this.createUser(details); 21 | } 22 | 23 | createUser(details: UserDetails) { 24 | const user = this.userRepo.create(details); 25 | return this.userRepo.save(user); 26 | } 27 | 28 | findUser(discordId: string): Promise { 29 | return this.userRepo.findOne({ discordId }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/auth/services/auth/auth.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../../../typeorm'; 2 | import { UserDetails } from '../../../utils/types'; 3 | 4 | export interface AuthenticationProvider { 5 | validateUser(details: UserDetails); 6 | createUser(details: UserDetails); 7 | findUser(discordId: string): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/utils/DiscordStrategy.ts: -------------------------------------------------------------------------------- 1 | import { Profile, Strategy } from 'passport-discord'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Inject, Injectable } from '@nestjs/common'; 4 | import { AuthenticationProvider } from '../services/auth/auth'; 5 | 6 | @Injectable() 7 | export class DiscordStrategy extends PassportStrategy(Strategy) { 8 | constructor( 9 | @Inject('AUTH_SERVICE') 10 | private readonly authService: AuthenticationProvider, 11 | ) { 12 | super({ 13 | clientID: process.env.DISCORD_CLIENT_ID, 14 | clientSecret: process.env.DISCORD_CLIENT_SECRET, 15 | callbackURL: process.env.DISCORD_CALLBACK_URL, 16 | scope: ['identify', 'guilds'], 17 | }); 18 | } 19 | 20 | async validate(accessToken: string, refreshToken: string, profile: Profile) { 21 | const { username, discriminator, id: discordId, avatar } = profile; 22 | const details = { 23 | username, 24 | discriminator, 25 | discordId, 26 | avatar, 27 | accessToken, 28 | refreshToken, 29 | }; 30 | return this.authService.validateUser(details); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/auth/utils/Guards.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { GqlExecutionContext } from '@nestjs/graphql'; 3 | import { AuthGuard } from '@nestjs/passport'; 4 | 5 | @Injectable() 6 | export class DiscordAuthGuard extends AuthGuard('discord') { 7 | async canActivate(context: ExecutionContext) { 8 | const activate = (await super.canActivate(context)) as boolean; 9 | const request = context.switchToHttp().getRequest(); 10 | await super.logIn(request); 11 | return activate; 12 | } 13 | } 14 | 15 | @Injectable() 16 | export class AuthenticatedGuard implements CanActivate { 17 | async canActivate(context: ExecutionContext): Promise { 18 | const req = context.switchToHttp().getRequest(); 19 | return req.isAuthenticated(); 20 | } 21 | } 22 | 23 | @Injectable() 24 | export class GraphQLAuthGuard implements CanActivate { 25 | canActivate(context: ExecutionContext) { 26 | const ctx = GqlExecutionContext.create(context); 27 | return ctx.getContext().req.user; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/auth/utils/Serializer.ts: -------------------------------------------------------------------------------- 1 | import { PassportSerializer } from '@nestjs/passport'; 2 | import { Inject, Injectable } from '@nestjs/common'; 3 | import { User } from '../../typeorm'; 4 | import { Done } from '../../utils/types'; 5 | import { AuthenticationProvider } from '../services/auth/auth'; 6 | 7 | @Injectable() 8 | export class SessionSerializer extends PassportSerializer { 9 | constructor( 10 | @Inject('AUTH_SERVICE') 11 | private readonly authService: AuthenticationProvider, 12 | ) { 13 | super(); 14 | } 15 | 16 | serializeUser(user: User, done: Done) { 17 | done(null, user); 18 | } 19 | 20 | async deserializeUser(user: User, done: Done) { 21 | const userDB = await this.authService.findUser(user.discordId); 22 | return userDB ? done(null, userDB) : done(null, null); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/discord/discord.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule, Module } from '@nestjs/common'; 2 | import { DiscordService } from './discord.service'; 3 | 4 | @Module({ 5 | imports: [HttpModule], 6 | providers: [ 7 | { 8 | provide: 'DISCORD_SERVICE', 9 | useClass: DiscordService, 10 | }, 11 | ], 12 | exports: [ 13 | { 14 | provide: 'DISCORD_SERVICE', 15 | useClass: DiscordService, 16 | }, 17 | ], 18 | }) 19 | export class DiscordModule {} 20 | -------------------------------------------------------------------------------- /src/discord/discord.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { DiscordService } from './discord.service'; 3 | 4 | describe('DiscordService', () => { 5 | let service: DiscordService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [DiscordService], 10 | }).compile(); 11 | 12 | service = module.get(DiscordService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/discord/discord.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpService, Inject, Injectable } from '@nestjs/common'; 2 | import { AxiosResponse } from 'axios'; 3 | import { Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | import { Guild } from 'src/graphql'; 6 | import { DiscordProvider } from './discord'; 7 | 8 | @Injectable() 9 | export class DiscordService implements DiscordProvider { 10 | constructor(@Inject(HttpService) private readonly httpService: HttpService) {} 11 | fetchGuilds(accessToken: string): Observable> { 12 | return this.httpService 13 | .get('http://discord.com/api/v8/users/@me/guilds', { 14 | headers: { 15 | Authorization: `Bearer ${accessToken}`, 16 | }, 17 | }) 18 | .pipe(map((response) => response.data)); 19 | } 20 | fetchGuildRoles(guildId: string) { 21 | throw new Error('Method not implemented.'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/discord/discord.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import { Observable } from 'rxjs'; 3 | import { Guild } from 'src/graphql'; 4 | 5 | export interface DiscordProvider { 6 | fetchGuilds(accessToken: string): Observable>; 7 | fetchGuildRoles(guildId: string); 8 | } 9 | -------------------------------------------------------------------------------- /src/graphql/index.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | getUser: User 3 | } 4 | 5 | type User { 6 | discordId: ID! 7 | username: String! 8 | avatar: String 9 | discriminator: String! 10 | guilds: [Guild] 11 | } 12 | 13 | type Guild { 14 | id: ID! 15 | name: String! 16 | icon: String 17 | description: String 18 | banner: String 19 | owner_id: String 20 | roles: [Role] 21 | } 22 | 23 | type Role { 24 | id: String! 25 | name: String! 26 | permissions: String! 27 | position: Int! 28 | color: Int! 29 | } 30 | -------------------------------------------------------------------------------- /src/graphql/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /** ------------------------------------------------------ 3 | * THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) 4 | * ------------------------------------------------------- 5 | */ 6 | 7 | /* tslint:disable */ 8 | /* eslint-disable */ 9 | export interface IQuery { 10 | getUser(): User | Promise; 11 | } 12 | 13 | export interface User { 14 | discordId: string; 15 | username: string; 16 | avatar?: string; 17 | discriminator: string; 18 | guilds?: Guild[]; 19 | } 20 | 21 | export interface Guild { 22 | id: string; 23 | name: string; 24 | icon?: string; 25 | description?: string; 26 | banner?: string; 27 | owner_id?: string; 28 | roles?: Role[]; 29 | } 30 | 31 | export interface Role { 32 | id: string; 33 | name: string; 34 | permissions: string; 35 | position: number; 36 | color: number; 37 | } 38 | -------------------------------------------------------------------------------- /src/graphql/resolvers/User.resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createParamDecorator, 3 | ExecutionContext, 4 | Inject, 5 | UseGuards, 6 | } from '@nestjs/common'; 7 | import { 8 | GqlExecutionContext, 9 | Parent, 10 | Query, 11 | ResolveField, 12 | Resolver, 13 | } from '@nestjs/graphql'; 14 | import { GraphQLAuthGuard } from 'src/auth/utils/Guards'; 15 | import { AuthenticationProvider } from 'src/auth/services/auth/auth'; 16 | import { User } from 'src/typeorm'; 17 | import { DiscordService } from 'src/discord/discord.service'; 18 | import { DiscordProvider } from 'src/discord/discord'; 19 | 20 | export const CurrentUser = createParamDecorator( 21 | (data: unknown, context: ExecutionContext) => { 22 | const ctx = GqlExecutionContext.create(context); 23 | return ctx.getContext().req.user; 24 | }, 25 | ); 26 | 27 | @Resolver('User') 28 | @UseGuards(GraphQLAuthGuard) 29 | export class UserResolver { 30 | constructor( 31 | @Inject('AUTH_SERVICE') 32 | private readonly authService: AuthenticationProvider, 33 | @Inject('DISCORD_SERVICE') 34 | private readonly discordService: DiscordProvider, 35 | ) {} 36 | 37 | @Query('getUser') 38 | async getUser(@CurrentUser() user: User): Promise { 39 | console.log(user); 40 | return user; 41 | } 42 | 43 | @ResolveField() 44 | async guilds(@Parent() user: User) { 45 | console.log(user); 46 | console.log('Guilds Resolve Field'); 47 | return this.discordService.fetchGuilds(user.accessToken); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import * as session from 'express-session'; 4 | import * as passport from 'passport'; 5 | import { getRepository } from 'typeorm'; 6 | import { TypeORMSession } from './typeorm/entities/Session'; 7 | import { TypeormStore } from 'connect-typeorm'; 8 | 9 | async function bootstrap() { 10 | const app = await NestFactory.create(AppModule); 11 | const PORT = process.env.PORT || 3003; 12 | const sessionRepo = getRepository(TypeORMSession); 13 | app.setGlobalPrefix('api'); 14 | app.enableCors({ 15 | origin: ['http://localhost:3000'], 16 | credentials: true, 17 | }); 18 | app.use( 19 | session({ 20 | cookie: { 21 | maxAge: 86400000, 22 | }, 23 | secret: 'dahdgasdjhsadgsajhdsagdhjd', 24 | resave: false, 25 | saveUninitialized: false, 26 | store: new TypeormStore().connect(sessionRepo), 27 | }), 28 | ); 29 | app.use(passport.initialize()); 30 | app.use(passport.session()); 31 | await app.listen(PORT, () => console.log(`Running on Port ${PORT}`)); 32 | } 33 | bootstrap(); 34 | -------------------------------------------------------------------------------- /src/typeorm/entities/Session.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 | -------------------------------------------------------------------------------- /src/typeorm/entities/User.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity({ name: 'users' }) 4 | export class User { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | 8 | @Column({ name: 'discord_id', unique: true }) 9 | discordId: string; 10 | 11 | @Column() 12 | username: string; 13 | 14 | @Column() 15 | discriminator: string; 16 | 17 | @Column({ nullable: true }) 18 | avatar: string; 19 | 20 | @Column({ name: 'access_token' }) 21 | accessToken: string; 22 | 23 | @Column({ name: 'refresh_token' }) 24 | refreshToken: string; 25 | } 26 | -------------------------------------------------------------------------------- /src/typeorm/index.ts: -------------------------------------------------------------------------------- 1 | import { TypeORMSession } from './entities/Session'; 2 | import { User } from './entities/User'; 3 | 4 | export const entities = [User, TypeORMSession]; 5 | 6 | export { User, TypeORMSession }; 7 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../typeorm'; 2 | 3 | export type UserDetails = { 4 | username: string; 5 | discriminator: string; 6 | discordId: string; 7 | avatar: string; 8 | accessToken: string; 9 | refreshToken: string; 10 | }; 11 | 12 | export type Done = (err: Error, user: User) => void; 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /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 | } 15 | } 16 | --------------------------------------------------------------------------------