diff --git a/migrations/20240717204437-add-google-id.js b/migrations/20240717204437-add-google-id.js new file mode 100644 index 0000000..24117db --- /dev/null +++ b/migrations/20240717204437-add-google-id.js @@ -0,0 +1,34 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up (queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'Users', + 'googleId', + { + type: Sequelize.STRING, + allowNull: true, + }, + { transaction } + ); + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + + async down (queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('Users', 'googleId', { transaction }); + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/models/user.model.ts b/models/user.model.ts index 77b9354..f1587d1 100644 --- a/models/user.model.ts +++ b/models/user.model.ts @@ -16,4 +16,7 @@ export class User extends Model { @Column password: string; + + @Column + googleId?: string; } diff --git a/src/app.module.ts b/src/app.module.ts index 0fecc16..f679e6b 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,6 +6,7 @@ import { SequelizeModule } from '@nestjs/sequelize'; import { UsersModule } from './users/users.module'; import { User } from '../models/user.model'; import { AuthModule } from './auth/auth.module'; +import { GoogleSheetsService } from './google-sheets/google-sheets.service'; @Module({ imports: [ @@ -26,6 +27,6 @@ import { AuthModule } from './auth/auth.module'; AuthModule, ], controllers: [AppController], - providers: [AppService], + providers: [AppService, GoogleSheetsService], }) export class AppModule {} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 6ab8c11..b1f9533 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,8 +1,9 @@ // auth/auth.controller.ts -import { Controller, Post, Body, UseGuards, Request } from '@nestjs/common'; +import { Controller, Post, Body, UseGuards, Request, Get, Req, Res, Redirect } from '@nestjs/common'; import { AuthService } from './auth.service'; import { JwtAuthGuard } from './jwt-auth.guard'; import { User } from '../../models/user.model'; +import { AuthGuard } from '@nestjs/passport'; @Controller('auth') export class AuthController { @@ -27,4 +28,21 @@ export class AuthController { getProfile(@Request() req) { return req.user; } + + @Get('google') + @UseGuards(AuthGuard('google')) + async googleAuth(@Req() req) { + // Guard initiates Google OAuth + } + + @Get('google/callback') + @UseGuards(AuthGuard('google')) + @Redirect('http://localhost:4200/google-callback', 302) + async googleAuthRedirect(@Req() req, @Res() res: Response) { + const user: User = req.user; + const token = await this.authService.login(user); + const frontendRedirectUrl = `http://localhost:4200/google-callback?token=${token.access_token}`; + console.log(frontendRedirectUrl); + return { url: frontendRedirectUrl }; + } } diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 267e9b4..4d2a502 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -6,6 +6,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { AuthService } from './auth.service'; import { AuthController } from './auth.controller'; import { JwtStrategy } from './jwt.strategy'; +import { GoogleStrategy } from './google.strategy'; import { UsersModule } from '../users/users.module'; @Module({ @@ -21,7 +22,7 @@ import { UsersModule } from '../users/users.module'; inject: [ConfigService], }), ], - providers: [AuthService, JwtStrategy], + providers: [AuthService, JwtStrategy, GoogleStrategy], controllers: [AuthController], }) export class AuthModule {} diff --git a/src/auth/google.strategy.ts b/src/auth/google.strategy.ts new file mode 100644 index 0000000..4ea3249 --- /dev/null +++ b/src/auth/google.strategy.ts @@ -0,0 +1,39 @@ +// auth/google.strategy.ts +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy, VerifyCallback } from 'passport-google-oauth20'; +import { ConfigService } from '@nestjs/config'; +import { UsersService } from '../users/users.service'; +import { User } from '../../models/user.model'; + +@Injectable() +export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { + constructor( + private readonly configService: ConfigService, + private readonly usersService: UsersService, + ) { + super({ + clientID: configService.get('GOOGLE_CLIENT_ID'), + clientSecret: configService.get('GOOGLE_CLIENT_SECRET'), + callbackURL: configService.get('GOOGLE_CALLBACK_URL'), + scope: ['email', 'profile'], + }); + } + + async validate( + accessToken: string, + refreshToken: string, + profile: any, + done: VerifyCallback, + ): Promise { + const { id, name, emails } = profile; + const user: Partial = { + googleId: id, + email: emails[0].value, + name: name.givenName + ' ' + name.familyName, + }; + + const userFromDb = await this.usersService.findOrCreate(user); + done(null, userFromDb); + } +} diff --git a/src/google-sheets/google-sheets.service.ts b/src/google-sheets/google-sheets.service.ts new file mode 100644 index 0000000..a2bac5a --- /dev/null +++ b/src/google-sheets/google-sheets.service.ts @@ -0,0 +1,50 @@ +// google-sheets/google-sheets.service.ts +import { Injectable } from '@nestjs/common'; +import { google, sheets_v4 } from 'googleapis'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class GoogleSheetsService { + private sheets: sheets_v4.Sheets; + + constructor(private readonly configService: ConfigService) { + const auth = new google.auth.OAuth2( + this.configService.get('GOOGLE_CLIENT_ID'), + this.configService.get('GOOGLE_CLIENT_SECRET'), + this.configService.get('GOOGLE_CALLBACK_URL'), + ); + this.sheets = google.sheets({ version: 'v4', auth }); + } + + async getSheetData(spreadsheetId: string, range: string) { + const response = await this.sheets.spreadsheets.values.get({ + spreadsheetId, + range, + }); + return response.data.values; + } + + async updateSheetData(spreadsheetId: string, range: string, values: any[]) { + const response = await this.sheets.spreadsheets.values.update({ + spreadsheetId, + range, + valueInputOption: 'RAW', + requestBody: { + values, + }, + }); + return response.data; + } + + async createSheet(title: string): Promise { + const request = { + requestBody: { + properties: { + title, + }, + }, + }; + const response = await this.sheets.spreadsheets.create(request); + return response.data.spreadsheetId; + } +} diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 56527eb..87440bd 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -30,6 +30,14 @@ export class UsersService { }); } + async findOrCreate(user: Partial): Promise { + const existingUser = await this.findByEmail(user.email); + if (existingUser) { + return existingUser; + } + return this.create(user as User); + } + create(user: User): Promise { return this.userModel.create(user); }