-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat
Added architecture for login with github (#184)
- Loading branch information
Showing
25 changed files
with
691 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<IStrategyResponse> { | ||
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<UserEntity> = { | ||
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<string> { | ||
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<UserEntity> { | ||
const user = await this.getUser({ _id: payload._id }); | ||
if (!user) throw new UnauthorizedException('User not found'); | ||
|
||
return user; | ||
} | ||
|
||
async decodeJwt<T>(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); | ||
} | ||
} |
Oops, something went wrong.