From 2e8bbc7d9a2af4798f879c9fc51c0ce79430307e Mon Sep 17 00:00:00 2001 From: chavda-bhavik Date: Mon, 6 Feb 2023 13:45:27 +0530 Subject: [PATCH] feat: Added architecture for login with github --- apps/api/package.json | 16 ++ apps/api/src/.env.development | 9 +- apps/api/src/.env.production | 9 +- apps/api/src/.env.test | 9 +- apps/api/src/.example.env | 9 +- apps/api/src/app.module.ts | 2 + apps/api/src/app/auth/auth.controller.ts | 51 ++++ apps/api/src/app/auth/auth.module.ts | 51 ++++ .../decorators/strategy-user.decorator.ts | 8 + .../api/src/app/auth/services/auth.service.ts | 110 +++++++++ .../auth/services/passport/github.strategy.ts | 72 ++++++ .../auth/services/passport/jwt.strategy.ts | 24 ++ apps/api/src/app/shared/constants.ts | 18 ++ .../app/shared/exceptions/api.exception.ts | 3 + .../project-not-assigned.exception.ts | 8 + .../exceptions/user-not-found.exception.ts | 8 + .../app/shared/framework/user.decorator.ts | 24 ++ apps/api/src/app/shared/shared.module.ts | 4 + apps/api/src/app/shared/types/auth.types.ts | 23 ++ apps/api/src/bootstrap.ts | 10 +- apps/api/src/config/env-validator.ts | 9 +- apps/api/src/types/env.d.ts | 1 + apps/demo/components/Actions/Actions.tsx | 2 +- .../demo/components/Pagination/Pagination.tsx | 2 +- pnpm-lock.yaml | 228 +++++++++++++++++- 25 files changed, 691 insertions(+), 19 deletions(-) create mode 100644 apps/api/src/app/auth/auth.controller.ts create mode 100644 apps/api/src/app/auth/auth.module.ts create mode 100644 apps/api/src/app/auth/decorators/strategy-user.decorator.ts create mode 100644 apps/api/src/app/auth/services/auth.service.ts create mode 100644 apps/api/src/app/auth/services/passport/github.strategy.ts create mode 100644 apps/api/src/app/auth/services/passport/jwt.strategy.ts create mode 100644 apps/api/src/app/shared/exceptions/api.exception.ts create mode 100644 apps/api/src/app/shared/exceptions/project-not-assigned.exception.ts create mode 100644 apps/api/src/app/shared/exceptions/user-not-found.exception.ts create mode 100644 apps/api/src/app/shared/framework/user.decorator.ts create mode 100644 apps/api/src/app/shared/types/auth.types.ts diff --git a/apps/api/package.json b/apps/api/package.json index c190ea578..a178c8e25 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -23,10 +23,13 @@ "@impler/shared": "^0.1.12", "@nestjs/common": "^9.1.2", "@nestjs/core": "^9.1.2", + "@nestjs/jwt": "^10.0.1", + "@nestjs/passport": "^9.0.0", "@nestjs/platform-express": "^9.1.2", "@nestjs/swagger": "^6.1.2", "@nestjs/terminus": "^9.1.3", "@sentry/node": "^7.19.0", + "@types/jsonwebtoken": "^9.0.1", "ajv": "^8.11.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0", @@ -34,19 +37,32 @@ "class-transformer": "^0.5.1", "class-validator": "^0.13.2", "compression": "^1.7.4", + "cookie-parser": "^1.4.6", "dotenv": "^16.0.2", "envalid": "^7.3.1", "fast-csv": "^4.3.6", + "jsonwebtoken": "^9.0.0", + "passport": "^0.6.0", + "passport-github2": "^0.1.12", + "passport-jwt": "^4.0.1", + "passport-oauth2": "^1.6.1", "rimraf": "^3.0.2", + "rxjs": "^7.8.0", "xlsx": "^0.18.5" }, "devDependencies": { "@nestjs/cli": "^9.1.5", + "@types/body-parser": "^1.19.2", "@types/chai": "^4.3.4", + "@types/cookie-parser": "^1.4.3", "@types/express": "^4.17.14", "@types/mocha": "^10.0.0", "@types/multer": "^1.4.7", "@types/node": "^18.7.18", + "@types/passport": "^1.0.11", + "@types/passport-github2": "^1.2.5", + "@types/passport-jwt": "^3.0.8", + "@types/passport-oauth2": "^1.4.11", "chai": "^4.3.7", "mocha": "^10.1.0", "nodemon": "^2.0.20", diff --git a/apps/api/src/.env.development b/apps/api/src/.env.development index 3b73c9eed..316be0eb3 100644 --- a/apps/api/src/.env.development +++ b/apps/api/src/.env.development @@ -1,6 +1,5 @@ NODE_ENV=local PORT=3000 -FRONT_BASE_URL=http://localhost:4200 RABBITMQ_CONN_URL=amqp://guest:guest@localhost:5672 # Database @@ -13,8 +12,16 @@ S3_BUCKET_NAME=impler AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= +# URLs +FRONT_BASE_URL=http://localhost:3500 +WEB_BASE_URL=http://localhost:4200 + # Security ACCESS_KEY= +GITHUB_OAUTH_CLIENT_ID= +GITHUB_OAUTH_CLIENT_SECRET= +GITHUB_OAUTH_REDIRECT= +CLIENT_SUCCESS_AUTH_REDIRECT= # Analytics SENTRY_DSN= diff --git a/apps/api/src/.env.production b/apps/api/src/.env.production index 3b73c9eed..316be0eb3 100644 --- a/apps/api/src/.env.production +++ b/apps/api/src/.env.production @@ -1,6 +1,5 @@ NODE_ENV=local PORT=3000 -FRONT_BASE_URL=http://localhost:4200 RABBITMQ_CONN_URL=amqp://guest:guest@localhost:5672 # Database @@ -13,8 +12,16 @@ S3_BUCKET_NAME=impler AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= +# URLs +FRONT_BASE_URL=http://localhost:3500 +WEB_BASE_URL=http://localhost:4200 + # Security ACCESS_KEY= +GITHUB_OAUTH_CLIENT_ID= +GITHUB_OAUTH_CLIENT_SECRET= +GITHUB_OAUTH_REDIRECT= +CLIENT_SUCCESS_AUTH_REDIRECT= # Analytics SENTRY_DSN= diff --git a/apps/api/src/.env.test b/apps/api/src/.env.test index 3b73c9eed..316be0eb3 100644 --- a/apps/api/src/.env.test +++ b/apps/api/src/.env.test @@ -1,6 +1,5 @@ NODE_ENV=local PORT=3000 -FRONT_BASE_URL=http://localhost:4200 RABBITMQ_CONN_URL=amqp://guest:guest@localhost:5672 # Database @@ -13,8 +12,16 @@ S3_BUCKET_NAME=impler AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= +# URLs +FRONT_BASE_URL=http://localhost:3500 +WEB_BASE_URL=http://localhost:4200 + # Security ACCESS_KEY= +GITHUB_OAUTH_CLIENT_ID= +GITHUB_OAUTH_CLIENT_SECRET= +GITHUB_OAUTH_REDIRECT= +CLIENT_SUCCESS_AUTH_REDIRECT= # Analytics SENTRY_DSN= diff --git a/apps/api/src/.example.env b/apps/api/src/.example.env index 7f56ce0b1..a68491aae 100644 --- a/apps/api/src/.example.env +++ b/apps/api/src/.example.env @@ -1,6 +1,5 @@ NODE_ENV=local PORT=3000 -FRONT_BASE_URL=http://localhost:4200 RABBITMQ_CONN_URL=amqp://guest:guest@localhost:5672 # Database @@ -11,5 +10,13 @@ S3_LOCAL_STACK=http://localhost:4566 S3_REGION=us-east-1 S3_BUCKET_NAME=impler +# URLs +FRONT_BASE_URL=http://localhost:4200 +WEB_BASE_URL=http://localhost:3500 + # Security ACCESS_KEY= +GITHUB_OAUTH_CLIENT_ID= +GITHUB_OAUTH_CLIENT_SECRET= +GITHUB_OAUTH_REDIRECT= +CLIENT_SUCCESS_AUTH_REDIRECT= \ No newline at end of file diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 6d4e75c07..70b742a83 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -10,6 +10,7 @@ import { MappingModule } from './app/mapping/mapping.module'; import { ReviewModule } from './app/review/review.module'; import { CommonModule } from './app/common/common.module'; import { HealthModule } from 'app/health/health.module'; +import { AuthModule } from './app/auth/auth.module'; const modules: Array | ForwardReference> = [ ProjectModule, @@ -21,6 +22,7 @@ const modules: Array | ForwardRefe ReviewModule, CommonModule, HealthModule, + AuthModule, ]; const providers = []; diff --git a/apps/api/src/app/auth/auth.controller.ts b/apps/api/src/app/auth/auth.controller.ts new file mode 100644 index 000000000..b71a7f256 --- /dev/null +++ b/apps/api/src/app/auth/auth.controller.ts @@ -0,0 +1,51 @@ +import { AuthGuard } from '@nestjs/passport'; +import { Response } from 'express'; +import { ApiTags, ApiExcludeController } from '@nestjs/swagger'; +import { ClassSerializerInterceptor, Controller, Get, Res, UseGuards, UseInterceptors } from '@nestjs/common'; +import { IJwtPayload } from '@impler/shared'; +import { IStrategyResponse } from '@shared/types/auth.types'; +import { UserSession } from '@shared/framework/user.decorator'; +import { ApiException } from '@shared/exceptions/api.exception'; +import { StrategyUser } from './decorators/strategy-user.decorator'; +import { CONSTANTS, COOKIE_CONFIG } from '@shared/constants'; + +@ApiTags('Auth') +@Controller('/auth') +@ApiExcludeController() +@UseInterceptors(ClassSerializerInterceptor) +export class AuthController { + @Get('/github') + githubAuth() { + if (!process.env.GITHUB_OAUTH_CLIENT_ID || !process.env.GITHUB_OAUTH_CLIENT_SECRET) { + throw new ApiException( + 'GitHub auth is not configured, please provide GITHUB_OAUTH_CLIENT_ID and GITHUB_OAUTH_CLIENT_SECRET as env variables' + ); + } + + return { + success: true, + }; + } + + @Get('/github/callback') + @UseGuards(AuthGuard('github')) + async githubCallback(@StrategyUser() strategyUser: IStrategyResponse, @Res() response: Response) { + if (!strategyUser || !strategyUser.token) { + return response.redirect(`${process.env.CLIENT_SUCCESS_AUTH_REDIRECT}?error=AuthenticationError`); + } + + let url = process.env.CLIENT_SUCCESS_AUTH_REDIRECT; + if (strategyUser.userCreated && strategyUser.showAddProject) { + url += `?showAddProject=true`; + } + + response.cookie(CONSTANTS.AUTH_COOKIE_NAME, strategyUser.token, COOKIE_CONFIG); + + return response.send(url); + } + + @Get('/user') + async user(@UserSession() user: IJwtPayload) { + return user; + } +} diff --git a/apps/api/src/app/auth/auth.module.ts b/apps/api/src/app/auth/auth.module.ts new file mode 100644 index 000000000..40102b0d9 --- /dev/null +++ b/apps/api/src/app/auth/auth.module.ts @@ -0,0 +1,51 @@ +import { MiddlewareConsumer, Module, NestModule, Provider, RequestMethod } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import * as passport from 'passport'; +import { CONSTANTS } from '@shared/constants'; +import { PassportModule } from '@nestjs/passport'; +import { AuthController } from './auth.controller'; +import { AuthService } from './services/auth.service'; +import { SharedModule } from '../shared/shared.module'; +import { GitHubStrategy } from './services/passport/github.strategy'; +import { JwtStrategy } from './services/passport/jwt.strategy'; + +const AUTH_STRATEGIES: Provider[] = [JwtStrategy]; + +if (process.env.GITHUB_OAUTH_CLIENT_ID) { + AUTH_STRATEGIES.push(GitHubStrategy); +} + +@Module({ + imports: [ + SharedModule, + PassportModule.register({ + defaultStrategy: 'jwt', + }), + JwtModule.register({ + secretOrKeyProvider: () => process.env.JWT_SECRET as string, + signOptions: { + expiresIn: CONSTANTS.maxAge, + }, + }), + ], + controllers: [AuthController], + providers: [AuthService, ...AUTH_STRATEGIES], + exports: [], +}) +export class AuthModule implements NestModule { + public configure(consumer: MiddlewareConsumer) { + if (process.env.GITHUB_OAUTH_CLIENT_ID) { + consumer + .apply( + passport.authenticate('github', { + session: false, + scope: ['user:email'], + }) + ) + .forRoutes({ + path: '/auth/github', + method: RequestMethod.GET, + }); + } + } +} diff --git a/apps/api/src/app/auth/decorators/strategy-user.decorator.ts b/apps/api/src/app/auth/decorators/strategy-user.decorator.ts new file mode 100644 index 000000000..abea52871 --- /dev/null +++ b/apps/api/src/app/auth/decorators/strategy-user.decorator.ts @@ -0,0 +1,8 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { IStrategyResponse } from '@shared/types/auth.types'; + +export const StrategyUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + + return request.user as IStrategyResponse; +}); diff --git a/apps/api/src/app/auth/services/auth.service.ts b/apps/api/src/app/auth/services/auth.service.ts new file mode 100644 index 000000000..597e4e472 --- /dev/null +++ b/apps/api/src/app/auth/services/auth.service.ts @@ -0,0 +1,110 @@ +import { JwtService } from '@nestjs/jwt'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { UserEntity, UserRepository, MemberRepository, MemberEntity } from '@impler/dal'; +import { IJwtPayload, MemberStatusEnum } from '@impler/shared'; +import { UserNotFoundException } from '@shared/exceptions/user-not-found.exception'; +import { IAuthenticationData, IStrategyResponse } from '@shared/types/auth.types'; + +@Injectable() +export class AuthService { + constructor( + private jwtService: JwtService, + private userRepository: UserRepository, + private memberRepository: MemberRepository + ) {} + + async authenticate({ profile, provider, _invitationId }: IAuthenticationData): Promise { + let showAddProject = false; + let userCreated = false; + // get or create the user + let user = await this.userRepository.findOne({ email: profile.email }); + if (!user) { + const userObj: Partial = { + email: profile.email, + firstName: profile.firstName, + lastName: profile.lastName, + profilePicture: profile.avatar_url, + ...(provider ? { tokens: [provider] } : {}), + }; + user = await this.userRepository.create(userObj); + userCreated = true; + } + if (!user) { + throw new UserNotFoundException(); + } + + // update member or get member + const member: MemberEntity = await this.memberRepository.findOne({ + $or: [{ _id: _invitationId }, { 'invite.email': profile.email }], + }); + + if (!member) { + // invitationId is not valid + showAddProject = true; + } else if (userCreated && member?._id) { + // accept invitation or add user to project only first time + await this.memberRepository.findOneAndUpdate( + { _id: member._id }, + { + _userId: user._id, + 'invite.answerDate': new Date(), + memberStatus: MemberStatusEnum.ACTIVE, + } + ); + } + + return { + user, + userCreated, + showAddProject, + token: await this.getSignedToken(user, member._projectId, member.role), + }; + } + + async refreshToken(userId: string) { + const user = await this.getUser({ _id: userId }); + if (!user) throw new UnauthorizedException('User not found'); + + return this.getSignedToken(user); + } + + async getSignedToken(user: UserEntity, _projectId?: string, role?: string): Promise { + return ( + `Bearer ` + + this.jwtService.sign( + { + _id: user._id, + _projectId, + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + profilePicture: user.profilePicture, + role, + }, + { + expiresIn: '30 days', + issuer: 'impler', + } + ) + ); + } + + async validateUser(payload: IJwtPayload): Promise { + const user = await this.getUser({ _id: payload._id }); + if (!user) throw new UnauthorizedException('User not found'); + + return user; + } + + async decodeJwt(token: string) { + return this.jwtService.decode(token) as T; + } + + async verifyJwt(jwt: string) { + return this.jwtService.verify(jwt); + } + + private async getUser({ _id }: { _id: string }) { + return await this.userRepository.findById(_id); + } +} diff --git a/apps/api/src/app/auth/services/passport/github.strategy.ts b/apps/api/src/app/auth/services/passport/github.strategy.ts new file mode 100644 index 000000000..978bf229a --- /dev/null +++ b/apps/api/src/app/auth/services/passport/github.strategy.ts @@ -0,0 +1,72 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import * as githubPassport from 'passport-github2'; +import { Metadata, StateStoreStoreCallback, StateStoreVerifyCallback } from 'passport-oauth2'; +import { AuthProviderEnum } from '@impler/shared'; +import { AuthService } from '../auth.service'; +import { IStrategyResponse } from '@shared/types/auth.types'; +import { CONSTANTS } from '@shared/constants'; + +@Injectable() +export class GitHubStrategy extends PassportStrategy(githubPassport.Strategy, 'github') { + constructor(private authService: AuthService) { + super({ + clientID: process.env.GITHUB_OAUTH_CLIENT_ID, + clientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET, + callbackURL: process.env.GITHUB_OAUTH_REDIRECT, + scope: ['user:email'], + passReqToCallback: true, + store: { + verify(req, state: string, meta: Metadata, callback: StateStoreVerifyCallback) { + callback(null, true, JSON.stringify(req.query)); + }, + store(req, meta: Metadata, callback: StateStoreStoreCallback) { + callback(null, JSON.stringify(req.query)); + }, + }, + }); + } + + async validate( + req: any, + accessToken: string, + _refreshToken: string, + githubProfile, + done: (err: string, data?: IStrategyResponse) => void + ) { + try { + const query = this.parseState(req); + const profileData = githubProfile._json; + // eslint-disable-next-line no-magic-numbers + const email = githubProfile.emails[0].value; + const response = await this.authService.authenticate({ + _invitationId: query?.invitationId, + profile: { + avatar_url: profileData.avatar_url || CONSTANTS.DEFAULT_USER_AVATAR, + email, + // eslint-disable-next-line no-magic-numbers + lastName: profileData.name ? profileData.name.split(' ').slice(-1).join(' ') : null, + // eslint-disable-next-line no-magic-numbers + firstName: profileData.name ? profileData.name.split(' ').slice(0, -1).join(' ') : profileData.login, + }, + provider: { + accessToken, + provider: AuthProviderEnum.GITHUB, + providerId: profileData.id, + }, + }); + + done(null, response); + } catch (err) { + done((err as Error).message); + } + } + + private parseState(req: any) { + try { + return JSON.parse(req.query.state); + } catch (e) { + return {}; + } + } +} diff --git a/apps/api/src/app/auth/services/passport/jwt.strategy.ts b/apps/api/src/app/auth/services/passport/jwt.strategy.ts new file mode 100644 index 000000000..5449c96dc --- /dev/null +++ b/apps/api/src/app/auth/services/passport/jwt.strategy.ts @@ -0,0 +1,24 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { IJwtPayload } from '@impler/shared'; +import { AuthService } from '../auth.service'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(private readonly authService: AuthService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: process.env.JWT_SECRET, + }); + } + + async validate(payload: IJwtPayload) { + const user = await this.authService.validateUser(payload); + if (!user) { + throw new UnauthorizedException(); + } + + return payload; + } +} diff --git a/apps/api/src/app/shared/constants.ts b/apps/api/src/app/shared/constants.ts index 236a4294a..d5b0f8ac3 100644 --- a/apps/api/src/app/shared/constants.ts +++ b/apps/api/src/app/shared/constants.ts @@ -1,3 +1,5 @@ +import { CookieOptions } from 'express'; + export const APIMessages = { FILE_TYPE_NOT_VALID: 'File type is not supported.', FILE_IS_EMPTY: 'File is empty', @@ -15,4 +17,20 @@ export const APIMessages = { IN_PROGRESS: 'You may landed to wrong place, This uploaded file processing is started already.', COMPLETED: 'You may landed to wrong place, This uploaded file is already completed, no more steps left to perform.', PROJECT_WITH_TEMPLATE_MISSING: 'Template not found with provided ProjectId and Template', + PROJECT_NOT_ASSIGNED: 'Project is not assigned to you', + USER_NOT_FOUND: 'User is not found', +}; + +export const CONSTANTS = { + AUTH_COOKIE_NAME: 'authentication', + // eslint-disable-next-line no-magic-numbers + maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days + DEFAULT_USER_AVATAR: 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y', +}; + +export const COOKIE_CONFIG: CookieOptions = { + httpOnly: true, + secure: true, + maxAge: CONSTANTS.maxAge, + sameSite: 'none', }; diff --git a/apps/api/src/app/shared/exceptions/api.exception.ts b/apps/api/src/app/shared/exceptions/api.exception.ts new file mode 100644 index 000000000..94acedff1 --- /dev/null +++ b/apps/api/src/app/shared/exceptions/api.exception.ts @@ -0,0 +1,3 @@ +import { BadRequestException } from '@nestjs/common'; + +export class ApiException extends BadRequestException {} diff --git a/apps/api/src/app/shared/exceptions/project-not-assigned.exception.ts b/apps/api/src/app/shared/exceptions/project-not-assigned.exception.ts new file mode 100644 index 000000000..e05136ceb --- /dev/null +++ b/apps/api/src/app/shared/exceptions/project-not-assigned.exception.ts @@ -0,0 +1,8 @@ +import { UnprocessableEntityException } from '@nestjs/common'; +import { APIMessages } from '../constants'; + +export class ProjectNotAssignedException extends UnprocessableEntityException { + constructor() { + super(APIMessages.PROJECT_NOT_ASSIGNED); + } +} diff --git a/apps/api/src/app/shared/exceptions/user-not-found.exception.ts b/apps/api/src/app/shared/exceptions/user-not-found.exception.ts new file mode 100644 index 000000000..4b3e3511f --- /dev/null +++ b/apps/api/src/app/shared/exceptions/user-not-found.exception.ts @@ -0,0 +1,8 @@ +import { UnprocessableEntityException } from '@nestjs/common'; +import { APIMessages } from '../constants'; + +export class UserNotFoundException extends UnprocessableEntityException { + constructor() { + super(APIMessages.USER_NOT_FOUND); + } +} diff --git a/apps/api/src/app/shared/framework/user.decorator.ts b/apps/api/src/app/shared/framework/user.decorator.ts new file mode 100644 index 000000000..987745405 --- /dev/null +++ b/apps/api/src/app/shared/framework/user.decorator.ts @@ -0,0 +1,24 @@ +/* eslint-disable no-magic-numbers */ +import * as jwt from 'jsonwebtoken'; +import { Request } from 'express'; +import { createParamDecorator, UnauthorizedException } from '@nestjs/common'; +import { CONSTANTS } from '@shared/constants'; + +export const UserSession = createParamDecorator((data, ctx) => { + let req: Request; + if (ctx.getType() === 'graphql') { + req = ctx.getArgs()[2].req; + } else { + req = ctx.switchToHttp().getRequest(); + } + + if (req.cookies && req.cookies[CONSTANTS.AUTH_COOKIE_NAME]) { + const tokenParts = req.cookies[CONSTANTS.AUTH_COOKIE_NAME].split(' '); + if (!tokenParts || tokenParts[0] !== 'Bearer' || !tokenParts[1]) throw new UnauthorizedException('bad_token'); + const user = jwt.decode(tokenParts[1], { json: true }); + + if (user) return user; + } + + throw new UnauthorizedException('bad_token'); +}); diff --git a/apps/api/src/app/shared/shared.module.ts b/apps/api/src/app/shared/shared.module.ts index 72ece5cc5..3564c4e49 100644 --- a/apps/api/src/app/shared/shared.module.ts +++ b/apps/api/src/app/shared/shared.module.ts @@ -8,6 +8,8 @@ import { ProjectRepository, TemplateRepository, UploadRepository, + UserRepository, + MemberRepository, } from '@impler/dal'; import { S3StorageService, StorageService } from '@impler/shared/dist/services/storage'; import { CSVFileService, ExcelFileService } from './file/file.service'; @@ -21,6 +23,8 @@ const DAL_MODELS = [ UploadRepository, MappingRepository, CommonRepository, + UserRepository, + MemberRepository, ]; const FILE_SERVICES = [CSVFileService, FileNameService, ExcelFileService]; diff --git a/apps/api/src/app/shared/types/auth.types.ts b/apps/api/src/app/shared/types/auth.types.ts new file mode 100644 index 000000000..1bdb3d62f --- /dev/null +++ b/apps/api/src/app/shared/types/auth.types.ts @@ -0,0 +1,23 @@ +import { AuthProviderEnum } from '@impler/shared'; + +export interface IAuthenticationData { + profile: { + email: string; + avatar_url: string; + firstName: string; + lastName: string; + }; + _invitationId?: string; + provider?: { + accessToken: string; + provider: AuthProviderEnum; + providerId: string; + }; +} + +export interface IStrategyResponse { + user: any; + userCreated: boolean; + showAddProject: boolean; + token: string; +} diff --git a/apps/api/src/bootstrap.ts b/apps/api/src/bootstrap.ts index bdef678aa..2242bc2e2 100644 --- a/apps/api/src/bootstrap.ts +++ b/apps/api/src/bootstrap.ts @@ -5,6 +5,7 @@ import { INestApplication, ValidationPipe, Logger } from '@nestjs/common'; import * as compression from 'compression'; import { NestFactory } from '@nestjs/core'; import * as bodyParser from 'body-parser'; +import * as cookieParser from 'cookie-parser'; import { ExpressAdapter } from '@nestjs/platform-express'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './app.module'; @@ -51,6 +52,7 @@ export async function bootstrap(expressApp?): Promise { }) ); + app.use(cookieParser()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); @@ -87,16 +89,12 @@ export async function bootstrap(expressApp?): Promise { const corsOptionsDelegate = function (req, callback) { const corsOptions = { - origin: false as boolean | string | string[], + credentials: true, + origin: [process.env.FRONT_BASE_URL, process.env.WEB_BASE_URL], preflightContinue: false, allowedHeaders: ['Content-Type', ACCESS_KEY_NAME, 'sentry-trace', 'baggage'], methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], }; - if (['dev', 'test', 'local'].includes(process.env.NODE_ENV)) { - corsOptions.origin = '*'; - } else { - corsOptions.origin = [process.env.FRONT_BASE_URL]; - } callback(null, corsOptions); }; diff --git a/apps/api/src/config/env-validator.ts b/apps/api/src/config/env-validator.ts index f66f81647..17c967031 100644 --- a/apps/api/src/config/env-validator.ts +++ b/apps/api/src/config/env-validator.ts @@ -12,11 +12,18 @@ const validators: { [K in keyof any]: ValidatorSpec } = { S3_BUCKET_NAME: str(), S3_REGION: str(), PORT: port(), - FRONT_BASE_URL: url(), MONGO_URL: str(), RABBITMQ_CONN_URL: str(), AWS_ACCESS_KEY_ID: str({ default: '' }), AWS_SECRET_ACCESS_KEY: str({ default: '' }), + // urls + FRONT_BASE_URL: url(), + WEB_BASE_URL: url(), + // auth + CLIENT_SUCCESS_AUTH_REDIRECT: str(), + GITHUB_OAUTH_CLIENT_ID: str(), + GITHUB_OAUTH_CLIENT_SECRET: str(), + GITHUB_OAUTH_REDIRECT: str(), }; export function validateEnv() { diff --git a/apps/api/src/types/env.d.ts b/apps/api/src/types/env.d.ts index 2b1ab7115..2a4ce7b86 100644 --- a/apps/api/src/types/env.d.ts +++ b/apps/api/src/types/env.d.ts @@ -5,6 +5,7 @@ declare namespace NodeJS { PORT: number; ACCESS_KEY?: string; FRONT_BASE_URL: string; + WEB_BASE_URL: string; SENTRY_DSN: string; MONGO_URL: string; S3_LOCAL_STACK: string; diff --git a/apps/demo/components/Actions/Actions.tsx b/apps/demo/components/Actions/Actions.tsx index 6dafd5be2..bb60e5b75 100644 --- a/apps/demo/components/Actions/Actions.tsx +++ b/apps/demo/components/Actions/Actions.tsx @@ -61,7 +61,7 @@ const Actions = ({ PROJECT_ID, ACCESS_TOKEN, PRIMARY_COLOR, TEMPLATE }: ActionPr label="Show Invalid Data" color="white" checked={showInvalidRecords} - onChange={(e) => onShowInvalidChanges(e.target.checked)} + onChange={(e: React.ChangeEvent) => onShowInvalidChanges(e.target.checked)} classNames={{ root: classes.root, track: classes.track, diff --git a/apps/demo/components/Pagination/Pagination.tsx b/apps/demo/components/Pagination/Pagination.tsx index 852b15b92..16615d9fe 100644 --- a/apps/demo/components/Pagination/Pagination.tsx +++ b/apps/demo/components/Pagination/Pagination.tsx @@ -39,7 +39,7 @@ const Pagination = ({ limit, onLimitChange, dataLength, page, setPage, totalPage size="xs" radius={0} value={limit} - onChange={(e) => onLimitChange(Number(e.target.value))} + onChange={(e: React.ChangeEvent) => onLimitChange(Number(e.target.value))} disabled={dataLength === variables.ZERO} classNames={{ input: classes.selectInput }} /> diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index edad34d18..4401b4a6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,15 +67,24 @@ importers: '@nestjs/cli': ^9.1.5 '@nestjs/common': ^9.1.2 '@nestjs/core': ^9.1.2 + '@nestjs/jwt': ^10.0.1 + '@nestjs/passport': ^9.0.0 '@nestjs/platform-express': ^9.1.2 '@nestjs/swagger': ^6.1.2 '@nestjs/terminus': ^9.1.3 '@sentry/node': ^7.19.0 + '@types/body-parser': ^1.19.2 '@types/chai': ^4.3.4 + '@types/cookie-parser': ^1.4.3 '@types/express': ^4.17.14 + '@types/jsonwebtoken': ^9.0.1 '@types/mocha': ^10.0.0 '@types/multer': ^1.4.7 '@types/node': ^18.7.18 + '@types/passport': ^1.0.11 + '@types/passport-github2': ^1.2.5 + '@types/passport-jwt': ^3.0.8 + '@types/passport-oauth2': ^1.4.11 ajv: ^8.11.0 ajv-formats: ^2.1.1 ajv-keywords: ^5.1.0 @@ -84,12 +93,19 @@ importers: class-transformer: ^0.5.1 class-validator: ^0.13.2 compression: ^1.7.4 + cookie-parser: ^1.4.6 dotenv: ^16.0.2 envalid: ^7.3.1 fast-csv: ^4.3.6 + jsonwebtoken: ^9.0.0 mocha: ^10.1.0 nodemon: ^2.0.20 + passport: ^0.6.0 + passport-github2: ^0.1.12 + passport-jwt: ^4.0.1 + passport-oauth2: ^1.6.1 rimraf: ^3.0.2 + rxjs: ^7.8.0 ts-loader: ^9.4.1 ts-node: ^10.9.1 tsconfig-paths: ^4.1.0 @@ -101,10 +117,13 @@ importers: '@impler/shared': link:../../libs/shared '@nestjs/common': 9.1.2_ufuujcqbszwis444rmt5m3dshy '@nestjs/core': 9.1.2_uesvjdiy3fmn5qqew6f6tmguiu + '@nestjs/jwt': 10.0.1_@nestjs+common@9.1.2 + '@nestjs/passport': 9.0.0_okkunj32hqcrg5gw7tl7a43dqq '@nestjs/platform-express': 9.1.2_umdjpi6fqr4wvw73r3mhthbyle '@nestjs/swagger': 6.1.2_o5ym4ozkuwx4tb6ybx3mgktlhe '@nestjs/terminus': 9.1.3_x7qim23dqjwxb5kho2jd5efavi '@sentry/node': 7.19.0 + '@types/jsonwebtoken': 9.0.1 ajv: 8.11.0 ajv-formats: 2.1.1_ajv@8.11.0 ajv-keywords: 5.1.0_ajv@8.11.0 @@ -112,18 +131,31 @@ importers: class-transformer: 0.5.1 class-validator: 0.13.2 compression: 1.7.4 + cookie-parser: 1.4.6 dotenv: 16.0.2 envalid: 7.3.1 fast-csv: 4.3.6 + jsonwebtoken: 9.0.0 + passport: 0.6.0 + passport-github2: 0.1.12 + passport-jwt: 4.0.1 + passport-oauth2: 1.6.1 rimraf: 3.0.2 + rxjs: 7.8.0 xlsx: 0.18.5 devDependencies: '@nestjs/cli': 9.1.5 + '@types/body-parser': 1.19.2 '@types/chai': 4.3.4 + '@types/cookie-parser': 1.4.3 '@types/express': 4.17.14 '@types/mocha': 10.0.0 '@types/multer': 1.4.7 '@types/node': 18.7.18 + '@types/passport': 1.0.11 + '@types/passport-github2': 1.2.5 + '@types/passport-jwt': 3.0.8 + '@types/passport-oauth2': 1.4.11 chai: 4.3.7 mocha: 10.1.0 nodemon: 2.0.20 @@ -6299,6 +6331,16 @@ packages: - encoding dev: false + /@nestjs/jwt/10.0.1_@nestjs+common@9.1.2: + resolution: {integrity: sha512-LwXBKVYHnFeX6GH/Wt0WDjsWCmNDC6tEdLlwNMAvJgYp+TkiCpEmQLkgRpifdUE29mvYSbjSnVs2kW2ob935NA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 + dependencies: + '@nestjs/common': 9.1.2_ufuujcqbszwis444rmt5m3dshy + '@types/jsonwebtoken': 8.5.9 + jsonwebtoken: 9.0.0 + dev: false + /@nestjs/mapped-types/1.1.0_zeb4gyhc74qs6lqbathv6jdp3i: resolution: {integrity: sha512-+2kSly4P1QI+9eGt+/uGyPdEG1hVz7nbpqPHWZVYgoqz8eOHljpXPag+UCVRw9zo2XCu4sgNUIGe8Uk0+OvUQg==} peerDependencies: @@ -6318,6 +6360,16 @@ packages: reflect-metadata: 0.1.13 dev: false + /@nestjs/passport/9.0.0_okkunj32hqcrg5gw7tl7a43dqq: + resolution: {integrity: sha512-Gnh8n1wzFPOLSS/94X1sUP4IRAoXTgG4odl7/AO5h+uwscEGXxJFercrZfqdAwkWhqkKWbsntM3j5mRy/6ZQDA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 + passport: ^0.4.0 || ^0.5.0 || ^0.6.0 + dependencies: + '@nestjs/common': 9.1.2_ufuujcqbszwis444rmt5m3dshy + passport: 0.6.0 + dev: false + /@nestjs/platform-express/9.1.2_umdjpi6fqr4wvw73r3mhthbyle: resolution: {integrity: sha512-SlAG6nfEVSk+lQL1Z4kjLip2jJ+w4mc8cG5ccM3mN06gREYfJVayNuydPUYtoxP/QmzugQfO5+cNEdqdhCmSQw==} peerDependencies: @@ -9050,7 +9102,7 @@ packages: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 18.7.18 + '@types/node': 18.11.18 /@types/bonjour/3.5.10: resolution: {integrity: sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==} @@ -9072,7 +9124,13 @@ packages: /@types/connect/3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 18.7.18 + '@types/node': 18.11.18 + + /@types/cookie-parser/1.4.3: + resolution: {integrity: sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==} + dependencies: + '@types/express': 4.17.14 + dev: true /@types/eslint-scope/3.7.4: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} @@ -9190,6 +9248,17 @@ packages: /@types/json5/0.0.29: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + /@types/jsonwebtoken/8.5.9: + resolution: {integrity: sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==} + dependencies: + '@types/node': 18.11.18 + dev: false + + /@types/jsonwebtoken/9.0.1: + resolution: {integrity: sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==} + dependencies: + '@types/node': 18.11.18 + /@types/lodash.camelcase/4.3.7: resolution: {integrity: sha512-Nfi6jpo9vuEOSIJP+mpbTezKyEt75DQlbwjiDvs/JctWkbnHDoyQo5lWqdvgNiJmVUjcmkfvlrvSEgJYvurOKg==} dependencies: @@ -9260,6 +9329,12 @@ packages: resolution: {integrity: sha512-WKG4gTr8przEZBiJ5r3s8ZIAoMXNbOgQ+j/d5O4X3x6kZJRLNvyUJuUK/KoG3+8BaOHPhp2m7WC6JKKeovDSzQ==} dev: false + /@types/oauth/0.9.1: + resolution: {integrity: sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==} + dependencies: + '@types/node': 18.11.18 + dev: true + /@types/parse-json/4.0.0: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} @@ -9267,6 +9342,43 @@ packages: resolution: {integrity: sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==} dev: false + /@types/passport-github2/1.2.5: + resolution: {integrity: sha512-+WLyrd8JPsCxroK34EjegR0j3FMxp6wqB9cw/sRCFkWT9qic1dymAn021gr336EpyjzdhjUd2KKrqyxvdFSvOA==} + dependencies: + '@types/express': 4.17.14 + '@types/passport': 1.0.11 + '@types/passport-oauth2': 1.4.11 + dev: true + + /@types/passport-jwt/3.0.8: + resolution: {integrity: sha512-VKJZDJUAHFhPHHYvxdqFcc5vlDht8Q2pL1/ePvKAgqRThDaCc84lSYOTQmnx3+JIkDlN+2KfhFhXIzlcVT+Pcw==} + dependencies: + '@types/express': 4.17.14 + '@types/jsonwebtoken': 9.0.1 + '@types/passport-strategy': 0.2.35 + dev: true + + /@types/passport-oauth2/1.4.11: + resolution: {integrity: sha512-KUNwmGhe/3xPbjkzkPwwcPmyFwfyiSgtV1qOrPBLaU4i4q9GSCdAOyCbkFG0gUxAyEmYwqo9OAF/rjPjJ6ImdA==} + dependencies: + '@types/express': 4.17.14 + '@types/oauth': 0.9.1 + '@types/passport': 1.0.11 + dev: true + + /@types/passport-strategy/0.2.35: + resolution: {integrity: sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==} + dependencies: + '@types/express': 4.17.14 + '@types/passport': 1.0.11 + dev: true + + /@types/passport/1.0.11: + resolution: {integrity: sha512-pz1cx9ptZvozyGKKKIPLcVDVHwae4hrH5d6g5J+DkMRRjR3cVETb4jMabhXAUbg3Ov7T22nFHEgaK2jj+5CBpw==} + dependencies: + '@types/express': 4.17.14 + dev: true + /@types/prettier/2.7.0: resolution: {integrity: sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A==} dev: false @@ -10947,7 +11059,7 @@ packages: engines: {node: '>=10', npm: '>=6'} dependencies: '@babel/runtime': 7.19.0 - cosmiconfig: 7.0.1 + cosmiconfig: 7.1.0 resolve: 1.22.1 dev: false @@ -11183,6 +11295,11 @@ packages: /base64-js/1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + /base64url/3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + dev: false + /basic-auth/2.0.1: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} @@ -11467,6 +11584,10 @@ packages: buffer: 5.7.1 dev: false + /buffer-equal-constant-time/1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + /buffer-from/0.1.2: resolution: {integrity: sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==} dev: false @@ -12397,10 +12518,23 @@ packages: /convert-source-map/1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + /cookie-parser/1.4.6: + resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==} + engines: {node: '>= 0.8.0'} + dependencies: + cookie: 0.4.1 + cookie-signature: 1.0.6 + dev: false + /cookie-signature/1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} dev: false + /cookie/0.4.1: + resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} + engines: {node: '>= 0.6'} + dev: false + /cookie/0.4.2: resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} engines: {node: '>= 0.6'} @@ -13497,6 +13631,12 @@ packages: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true + /ecdsa-sig-formatter/1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /ee-first/1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: false @@ -15252,7 +15392,7 @@ packages: minimatch: 3.1.2 node-abort-controller: 3.0.1 schema-utils: 3.1.1 - semver: 7.3.7 + semver: 7.3.8 tapable: 2.2.1 typescript: 4.8.4 webpack: 5.74.0 @@ -16481,7 +16621,7 @@ packages: mute-stream: 0.0.8 ora: 5.4.1 run-async: 2.4.1 - rxjs: 7.5.6 + rxjs: 7.8.0 string-width: 4.2.3 strip-ansi: 6.0.1 through: 2.3.8 @@ -17991,7 +18131,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.11.9 + '@types/node': 18.11.18 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -18183,6 +18323,16 @@ packages: engines: {node: '>=0.10.0'} dev: false + /jsonwebtoken/9.0.0: + resolution: {integrity: sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash: 4.17.21 + ms: 2.1.3 + semver: 7.3.8 + dev: false + /jsx-ast-utils/3.3.3: resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==} engines: {node: '>=4.0'} @@ -18203,6 +18353,21 @@ packages: resolution: {integrity: sha512-6ufhP9SHjb7jibNFrNxyFZ6od3g+An6Ai9mhGRvcYe8UJlH0prseN64M+6ZBBUoKYHZsitDP42gAJ8+eVWr3lw==} dev: true + /jwa/1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws/3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + /kareem/2.4.1: resolution: {integrity: sha512-aJ9opVoXroQUPfovYP5kaj2lM7Jn02Gw13bL0lg9v0V7SaUc0qavPs0Eue7d2DcC3NjqI6QAUElXNsuZSeM+EA==} dev: false @@ -19944,6 +20109,10 @@ packages: - debug dev: true + /oauth/0.9.15: + resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} + dev: false + /object-assign/4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -20490,6 +20659,45 @@ packages: engines: {node: '>=0.10.0'} dev: false + /passport-github2/0.1.12: + resolution: {integrity: sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==} + engines: {node: '>= 0.8.0'} + dependencies: + passport-oauth2: 1.6.1 + dev: false + + /passport-jwt/4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + dependencies: + jsonwebtoken: 9.0.0 + passport-strategy: 1.0.0 + dev: false + + /passport-oauth2/1.6.1: + resolution: {integrity: sha512-ZbV43Hq9d/SBSYQ22GOiglFsjsD1YY/qdiptA+8ej+9C1dL1TVB+mBE5kDH/D4AJo50+2i8f4bx0vg4/yDDZCQ==} + engines: {node: '>= 0.4.0'} + dependencies: + base64url: 3.0.1 + oauth: 0.9.15 + passport-strategy: 1.0.0 + uid2: 0.0.4 + utils-merge: 1.0.1 + dev: false + + /passport-strategy/1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + dev: false + + /passport/0.6.0: + resolution: {integrity: sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==} + engines: {node: '>= 0.4.0'} + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 + dev: false + /path-browserify/0.0.1: resolution: {integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==} dev: false @@ -20567,6 +20775,10 @@ packages: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} dev: true + /pause/0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + dev: false + /pbkdf2/3.1.2: resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} engines: {node: '>=0.12'} @@ -24828,6 +25040,10 @@ packages: requiresBuild: true optional: true + /uid2/0.0.4: + resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==} + dev: false + /unbox-primitive/1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: