diff --git a/.env.example b/.env.example index 6a5487ac..da04fb55 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,11 @@ DATABASE_URL=postgresql://postgres:@db..supabase.co SUPABASE_ANON_KEY= +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GITHUB_CALLBACK_URL= + + SMTP_HOST= SMTP_PORT= SMTP_EMAIL_ADDRESS= diff --git a/apps/api/src/auth/auth.module.ts b/apps/api/src/auth/auth.module.ts index 1c63412c..01b9605b 100644 --- a/apps/api/src/auth/auth.module.ts +++ b/apps/api/src/auth/auth.module.ts @@ -3,6 +3,7 @@ import { AuthService } from './service/auth.service' import { AuthController } from './controller/auth.controller' import { JwtModule } from '@nestjs/jwt' import { UserModule } from '../user/user.module' +import { GithubStrategy } from './auth.stratergy' @Module({ imports: [ @@ -17,7 +18,7 @@ import { UserModule } from '../user/user.module' }), UserModule ], - providers: [AuthService], + providers: [AuthService, GithubStrategy], controllers: [AuthController] }) export class AuthModule {} diff --git a/apps/api/src/auth/auth.stratergy.ts b/apps/api/src/auth/auth.stratergy.ts new file mode 100644 index 00000000..5b0854df --- /dev/null +++ b/apps/api/src/auth/auth.stratergy.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' +import { PassportStrategy } from '@nestjs/passport' +import { Profile, Strategy } from 'passport-github2' + +@Injectable() +export class GithubStrategy extends PassportStrategy(Strategy, 'github') { + constructor(configService: ConfigService) { + super({ + clientID: configService.get('GITHUB_CLIENT_ID'), + clientSecret: configService.get('GITHUB_CLIENT_SECRET'), + callbackURL: configService.get('GITHUB_CALLBACK_URL'), + scope: ['public_profile', 'user:email'] + }) + } + + async validate(accessToken: string, _refreshToken: string, profile: Profile) { + return profile + } +} diff --git a/apps/api/src/auth/controller/auth.controller.ts b/apps/api/src/auth/controller/auth.controller.ts index 9ef327b2..a1fa2bda 100644 --- a/apps/api/src/auth/controller/auth.controller.ts +++ b/apps/api/src/auth/controller/auth.controller.ts @@ -1,8 +1,18 @@ -import { Controller, HttpStatus, Param, Post, Query } from '@nestjs/common' +import { + Controller, + Get, + HttpStatus, + Param, + Post, + Query, + Req, + UseGuards +} from '@nestjs/common' import { AuthService } from '../service/auth.service' import { UserAuthenticatedResponse } from '../auth.types' import { Public } from '../../decorators/public.decorator' import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger' +import { AuthGuard } from '@nestjs/passport' @ApiTags('Auth Controller') @Controller('auth') @@ -71,4 +81,31 @@ export class AuthController { ): Promise { return await this.authService.validateOtp(email, otp) } + + @Public() + @Get('github') + @UseGuards(AuthGuard('github')) + @ApiOperation({ + summary: 'Github OAuth', + description: + 'This endpoint validates Github OAuth. If the OAuth is valid, it returns a valid token along with the user details' + }) + // TODO: add params, response and function return types here + async githubOAuthLogin() {} + + @Public() + @Get('github/callback') + @UseGuards(AuthGuard('github')) + @ApiOperation({ + summary: 'Github OAuth Callback', + description: + 'This endpoint validates Github OAuth. If the OAuth is valid, it returns a valid token along with the user details' + }) + // TODO: add params, response and function return types here + async githubOAuthCallback(@Req() req) { + const user = req.user + const email = user.emails[0].value + console.log(email) + return await this.authService.handleGithubOAuth(email) + } } diff --git a/apps/api/src/auth/service/auth.service.ts b/apps/api/src/auth/service/auth.service.ts index a391cf54..3871b202 100644 --- a/apps/api/src/auth/service/auth.service.ts +++ b/apps/api/src/auth/service/auth.service.ts @@ -29,19 +29,12 @@ export class AuthService { this.logger = new Logger(AuthService.name) } - async sendOtp(email: string): Promise { - if (!email || !email.includes('@')) { - this.logger.error(`Invalid email address: ${email}`) - throw new HttpException( - 'Please enter a valid email address', - HttpStatus.BAD_REQUEST - ) - } - + private async createUserIfNotExists(email: string) { + let user = await this.findUserByEmail(email) // We need to create the user if it doesn't exist yet - if (!(await this.findUserByEmail(email))) { + if (!user) { // Create the user - const user = await this.prisma.user.create({ + user = await this.prisma.user.create({ data: { email } @@ -62,7 +55,18 @@ export class AuthService { } }) } + return user + } + async sendOtp(email: string): Promise { + if (!email || !email.includes('@')) { + this.logger.error(`Invalid email address: ${email}`) + throw new HttpException( + 'Please enter a valid email address', + HttpStatus.BAD_REQUEST + ) + } + await this.createUserIfNotExists(email) const otp = await this.prisma.otp.create({ data: { code: randomUUID().slice(0, 6).toUpperCase(), @@ -126,6 +130,16 @@ export class AuthService { } } + async handleGithubOAuth(email: string) { + // We need to create the user if it doesn't exist yet + const user = await this.createUserIfNotExists(email) + + return { + ...user, + token: await this.jwt.signAsync({ id: user.id }) + } + } + @Cron(CronExpression.EVERY_HOUR) async cleanUpExpiredOtps() { try { diff --git a/package.json b/package.json index df7b21a8..36f9fedf 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,8 @@ "moment": "^2.30.1", "next": "14.0.4", "nodemailer": "^6.9.8", + "passport": "^0.7.0", + "passport-github2": "^0.1.12", "passport-jwt": "^4.0.1", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b3e5587..01281479 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,12 @@ importers: nodemailer: specifier: ^6.9.8 version: 6.9.8 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-github2: + specifier: ^0.1.12 + version: 0.1.12 passport-jwt: specifier: ^4.0.1 version: 4.0.1 @@ -235,12 +241,6 @@ importers: specifier: ^4.1.1 version: 4.1.1(webpack@5.89.0) - apps/cli: - dependencies: - tslib: - specifier: ^2.3.0 - version: 2.6.2 - packages/sdk-node: {} packages: @@ -5321,6 +5321,11 @@ packages: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: true + /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'} @@ -9416,6 +9421,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'} @@ -9667,6 +9676,13 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + /passport-github2@0.1.12: + resolution: {integrity: sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==} + engines: {node: '>= 0.8.0'} + dependencies: + passport-oauth2: 1.7.0 + dev: false + /passport-jwt@4.0.1: resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} dependencies: @@ -9674,6 +9690,17 @@ packages: passport-strategy: 1.0.0 dev: false + /passport-oauth2@1.7.0: + resolution: {integrity: sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==} + 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'} @@ -11580,6 +11607,10 @@ packages: dev: false optional: true + /uid2@0.0.4: + resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==} + dev: false + /uid@2.0.2: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'}