Skip to content

Commit

Permalink
feat: Add google auth for signin
Browse files Browse the repository at this point in the history
  • Loading branch information
GSinseswa721 committed May 7, 2024
1 parent b775d65 commit 762bf52
Show file tree
Hide file tree
Showing 16 changed files with 292 additions and 33 deletions.
13 changes: 0 additions & 13 deletions .env.example

This file was deleted.

15 changes: 14 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,22 @@
"@types/swagger-ui-express": "^4.1.6",
"bcrypt": "^5.1.1",
"class-validator": "^0.14.1",
"cloudinary": "^2.2.0",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-session": "^1.18.0",
"express-winston": "^4.2.0",
"highlight.js": "^11.9.0",
"jsend": "^1.1.0",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"nodemon": "^3.1.0",
"passport": "^0.7.0",
"passport-facebook": "^3.0.0",
"passport-google-oauth": "^2.0.0",
"passport-google-oauth20": "^2.0.0",
"pg": "^8.11.5",
"reflect-metadata": "^0.2.2",
"source-map-support": "^0.5.21",
Expand All @@ -53,10 +61,15 @@
"@types/eslint": "^8.56.10",
"@types/eslint__js": "^8.42.3",
"@types/express": "^4.17.21",
"@types/express-session": "^1.18.0",
"@types/jest": "^29.5.12",
"@types/jsend": "^1.0.32",
"@types/jsonwebtoken": "^9.0.6",
"@types/morgan": "^1.9.9",
"@types/node": "^20.12.7",
"@types/passport": "^1.0.16",
"@types/passport-facebook": "^3.0.3",
"@types/passport-google-oauth20": "^2.0.14",
"@types/reflect-metadata": "^0.1.0",
"@types/supertest": "^6.0.2",
"@types/winston": "^2.4.4",
Expand All @@ -75,4 +88,4 @@
"typescript": "^5.4.5",
"typescript-eslint": "^7.7.1"
}
}
}
24 changes: 24 additions & 0 deletions src/__test__/gmailController.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Request, Response } from 'express';
import { initiateGoogleLogin } from '../controllers/google.auth';

describe('initiateGoogleLogin', () => {
const req = {} as Request;
const res = {
redirect: jest.fn(),
} as unknown as Response;
const next = jest.fn();

it('should redirect to Google OAuth with appropriate scope', () => {
initiateGoogleLogin(req, res, next);

expect(res.redirect).toHaveBeenCalledWith(
expect.stringContaining('google')
);
expect(res.redirect).toHaveBeenCalledWith(
expect.stringContaining('profile')
);
expect(res.redirect).toHaveBeenCalledWith(
expect.stringContaining('email')
);
});
});
4 changes: 2 additions & 2 deletions src/__test__/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import request from 'supertest';
import { app, server } from '../index'; // update this with the path to your app file
import { app} from '../index'; // update this with the path to your app file
import { createConnection, getConnection, getConnectionOptions } from 'typeorm';
import { User } from '../entities/User';
import { getRepository, Repository } from 'typeorm';

Check warning on line 5 in src/__test__/route.test.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'Repository' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 5 in src/__test__/route.test.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'Repository' is defined but never used
Expand All @@ -12,7 +12,7 @@ beforeAll(async () => {

afterAll(async () => {
await getConnection('testConnection').close();
server.close();
// server.close();
});


Expand Down
34 changes: 34 additions & 0 deletions src/configs/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const dotenv = require('dotenv');

dotenv.config();

const getPrefix = () => {

Check warning on line 6 in src/configs/config.js

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

Missing return type on function
const env = process.env.NODE_ENV || 'production';
const envPrefixMap = {
development: 'DATABASE',
test: 'TEST_DATABASE',
production: 'PROD_DATABASE',
};
const prefix = envPrefixMap[env];
return prefix;
};

const getDatabaseConfig = () => {

Check warning on line 17 in src/configs/config.js

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

Missing return type on function
const prefix = getPrefix();
const config = {
secret: process.env.JWT_SECRET || 'secret',
};

if (prefix === 'PROD_DATABASE') {
config.dialectOptions = {
ssl: {
require: true,
rejectUnauthorized: false,
},
};
}
return config;
};

module.exports = getDatabaseConfig;
2 changes: 1 addition & 1 deletion src/controllers/authController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Request, Response } from 'express';
import { User } from '../entities/User';;
import { User } from '../entities/User';
import bcrypt from 'bcrypt';
import { getRepository } from 'typeorm';

Expand Down
43 changes: 43 additions & 0 deletions src/controllers/google.auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* eslint-disable no-console */
import { Request, Response, NextFunction } from 'express';
import passport from 'passport';
import { User } from '../entities/User';
import { responseSuccess, signToken } from '../utils';

export const initiateGoogleLogin = (
req: Request,
res: Response,
next: NextFunction
) => {

Check warning on line 11 in src/controllers/google.auth.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

Missing return type on function
passport.authenticate('google', { scope: ['profile', 'email'] })(
req,
res,
next
);
};

export const handleGoogleCallback = async (req: Request, res: Response) => {

Check warning on line 19 in src/controllers/google.auth.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

Missing return type on function
passport.authenticate(
'google',
async (err: unknown, user: User | null) => {
if (err) {
return res.status(500).json({error: 'Failed to authenticate with Google'});
}
if (!user) {
return res.status(401).json({error: 'User not found'});
}

try {
const token = signToken({ id: user.id });

Check warning on line 32 in src/controllers/google.auth.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

Trailing spaces not allowed
responseSuccess(res, 200, token, 'User authenticated successfully');
} catch (error) {
console.error('Error occurred:', error);
res.status(500).json({ error: 'Internal server error' });
}

}
)(req, res);
};

export { passport };
27 changes: 27 additions & 0 deletions src/entities/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ import {
@Entity()
@Unique(['email'])
export class User {
static User: any;
save() {
throw new Error('Method not implemented.');
}
static create(arg0: { accountId: string; name: string; provider: string; }): User | PromiseLike<User | null> | null {
throw new Error('Method not implemented.');
}
static findOne(arg0: { accountId: any; provider: string; }) {
throw new Error('Method not implemented.');
}
@PrimaryGeneratedColumn('uuid')
@IsNotEmpty()
id!: string;
Expand Down Expand Up @@ -66,4 +76,21 @@ import {

@UpdateDateColumn()
updatedAt!: Date;

@Column({ nullable: true })
@IsString()
accountId?: string;

@Column({ nullable: true })
@IsString()
name?: string;

@Column({ nullable: true })
@IsString()
photoURL?: string;

@Column({ nullable: true })
@IsString()
provider?: string;

}
21 changes: 13 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import express, { Request, Response } from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import router from './routes';
import { addDocumentation } from './startups/docs';
import 'reflect-metadata';


import dotenv from 'dotenv';
dotenv.config();
import { CustomError, errorHandler } from './middlewares/errorHandler';
import morgan from 'morgan';
import { dbConnection } from './startups/dbConnection';
import passport from 'passport';
import session from 'express-session';
import routerGmail from '../src/routes/googleRoutes.auth';
dotenv.config();

export const app = express();
const port = process.env.PORT || 8000;
app.use(express.json());

app.use(cors({ origin: '*' }));
app.use(router);
Expand All @@ -32,6 +32,11 @@ dbConnection();
const morganFormat = ':method :url :status :response-time ms - :res[content-length]';
app.use(morgan(morganFormat));

export const server = app.listen(port, () => {
console.log(`[server]: Server is running at http://localhost:${port}`);
});

//Google OAuth routes
app.use('/auth/google', routerGmail);

const port = process.env.PORT || 6890;
app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
});
9 changes: 9 additions & 0 deletions src/routes/googleRoutes.auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Router } from 'express';
import * as authController from '../controllers/google.auth';

const routerGmail = Router();

routerGmail.get('/users/google-auth', authController.initiateGoogleLogin);
routerGmail.get('/users/google-auth/callback', authController.handleGoogleCallback);

export default routerGmail;
59 changes: 59 additions & 0 deletions src/utils/cloud.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* eslint-disable import/no-extraneous-dependencies */

Check failure on line 1 in src/utils/cloud.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

Definition for rule 'import/no-extraneous-dependencies' was not found
import {
v2 as cloudinary,
UploadApiResponse,
UploadApiErrorResponse,
} from 'cloudinary';
import dotenv from 'dotenv';
import path from 'path';
import fs from 'fs';
import os from 'os';

dotenv.config();

cloudinary.config({
cloud_name: process.env.CLOUDINARY_NAME,
api_key: process.env.CLOUDINARY_KEY,
api_secret: process.env.CLOUDINARY_SECRET,
});

export const cloudUpload = async (
buffer: Buffer,
fileName: string
): Promise<string> => {
return new Promise((resolve, reject) => {
const tempFilePath = path.join(os.tmpdir(), fileName);

fs.writeFile(tempFilePath, buffer, writeError => {
if (writeError) {
reject(writeError);
return;
}

cloudinary.uploader.upload(
tempFilePath,
(
uploadError: UploadApiErrorResponse | undefined,
result: UploadApiResponse
) => {

if (uploadError) {
reject(new Error(uploadError.message));
} else {
const uploadResult: string = result.secure_url;
resolve(uploadResult);
}
}
);
});
});
};
export const deleteCloudinaryFile = async (url: string) => {
try {
await cloudinary.uploader.destroy(url);
return true;
} catch (error) {
return error;
}
};

5 changes: 5 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
// export all utils
export * from './response.utils';
export * from './logger';
export * from './cloud';
export * from './jwtToken';

27 changes: 27 additions & 0 deletions src/utils/jwtToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import jwt from 'jsonwebtoken';
import getDatabaseConfig from '../configs/config';


const { secret } = getDatabaseConfig();

// Verifies a token
export const verifyToken = (
token: string
): { email: string; id: string } | null => {
const decoded = jwt.verify(token, secret) as { email: string; id: string };
return decoded;
};

// Generates a token
interface TokenPayload {
id?: string;
email?: string;
}

export const signToken = (
payload: TokenPayload,
expiresIn: string = '2h'
): string => {
const token = jwt.sign(payload, secret, { expiresIn });
return token;
};
Loading

0 comments on commit 762bf52

Please sign in to comment.