From 3e7c4d5fa4aad742b76f3610f947d1e68bffbe09 Mon Sep 17 00:00:00 2001 From: Mahmoud Hamdy Date: Thu, 22 Aug 2024 02:35:41 +0300 Subject: [PATCH] feat: add auth with google --- src/controllers/auth.controller.ts | 62 +++++++++++++++++++- src/controllers/index.ts | 3 +- src/middlewares/validators/auth.validator.ts | 19 ++++++ src/middlewares/validators/index.ts | 1 + src/routes/authRoutes/auth.route.ts | 7 ++- src/types.d.ts | 6 ++ src/utils/index.ts | 36 ++++++++---- 7 files changed, 116 insertions(+), 18 deletions(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 66b5283..0a1da8a 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -4,6 +4,7 @@ import { generateJWT, passToExpressErrorHandler, verifyRefreshToken, + decodeGoogleCredential, } from "../utils"; import createError from "http-errors"; import bcrypt from "bcryptjs"; @@ -161,4 +162,63 @@ export const githubAuthenticator = async ( } catch (err: any) { passToExpressErrorHandler(err, next); } -}; \ No newline at end of file +}; + +export const googleAuthenticator = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { googleAuthCode } = req.body; + + const { name, email, googleId } = decodeGoogleCredential(googleAuthCode); + let user; + + const alreadyExistingUser = await prisma.user.findFirst({ + where: { + OR: [{ email }, { authType: "GOOGLE", authProviderId: googleId }], + }, + }); + + if ( + alreadyExistingUser && + ((email && !alreadyExistingUser.email) || + (alreadyExistingUser.authType !== "GOOGLE" && + alreadyExistingUser.authProviderId !== googleId) || + alreadyExistingUser.name !== name) + ) { + user = await prisma.user.update({ + where: { id: alreadyExistingUser.id }, + data: { + name, + email, + authType: "GOOGLE", + authProviderId: googleId, + }, + }); + } else if (alreadyExistingUser) { + res.status(200).json({ + accessToken: generateJWT(alreadyExistingUser.id, email, "access"), + refreshToken: generateJWT(alreadyExistingUser.id, email, "refresh"), + }); + return; + } else { + user = await prisma.user.create({ + data: { + name, + email, + authType: "GOOGLE", + authProviderId: googleId, + }, + }); + } + + res.status(200).json({ + accessToken: generateJWT(user.id, email, "access"), + refreshToken: generateJWT(user.id, email, "refresh"), + }); + } catch (err: any) { + passToExpressErrorHandler(err, next); + } +}; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 663c915..3c5f7c0 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -9,7 +9,8 @@ export { login, signup, refreshToken, - githubAuthenticator + githubAuthenticator, + googleAuthenticator } from "./auth.controller"; export { diff --git a/src/middlewares/validators/auth.validator.ts b/src/middlewares/validators/auth.validator.ts index c447df9..0c3d8f7 100644 --- a/src/middlewares/validators/auth.validator.ts +++ b/src/middlewares/validators/auth.validator.ts @@ -55,4 +55,23 @@ export const githubAuthValidator = async ( err.statusCode = 400; passToExpressErrorHandler(err, next); } +}; + +const googleAuthSchema = Joi.object({ + googleAuthCode: Joi.string().required(), +}); + +export const googleAuthValidator = async ( + req: Request, + _res: Response, + next: NextFunction +) => { + try { + + await googleAuthSchema.validateAsync(req.body); + next(); + } catch (err: any) { + err.statusCode = 400; + passToExpressErrorHandler(err, next); + } }; \ No newline at end of file diff --git a/src/middlewares/validators/index.ts b/src/middlewares/validators/index.ts index fb8b919..b856da7 100644 --- a/src/middlewares/validators/index.ts +++ b/src/middlewares/validators/index.ts @@ -2,5 +2,6 @@ export { authSignupValidator, authRefreshTokenValidator, githubAuthValidator, + googleAuthValidator, } from "./auth.validator"; export { claimPostValidator } from "./claim.validator"; diff --git a/src/routes/authRoutes/auth.route.ts b/src/routes/authRoutes/auth.route.ts index 3b8ba49..e5b05b5 100644 --- a/src/routes/authRoutes/auth.route.ts +++ b/src/routes/authRoutes/auth.route.ts @@ -3,13 +3,13 @@ import express from "express"; import { authSignupValidator, authRefreshTokenValidator, - githubAuthValidator, + googleAuthValidator, } from "../../middlewares/validators"; import { signup, login, refreshToken, - githubAuthenticator, + googleAuthenticator, } from "../../controllers"; const router = express.Router(); @@ -17,6 +17,7 @@ const router = express.Router(); router.post("/signup", authSignupValidator, signup); router.post("/login", login); router.post("/refresh_token", authRefreshTokenValidator, refreshToken); -router.post("/github", githubAuthValidator, githubAuthenticator); +// router.post("/github", githubAuthValidator, githubAuthenticator); +router.post("/google", googleAuthValidator, googleAuthenticator); export default router; diff --git a/src/types.d.ts b/src/types.d.ts index a22851a..8fdbeb1 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -13,6 +13,12 @@ declare global { iss: string; } + export interface GoogleCredentialDecoded { + name: string; + email: string; + sub: string; + } + export interface ModifiedRequest extends Request { isAuthenticated: boolean; userId: number; diff --git a/src/utils/index.ts b/src/utils/index.ts index f0e1a7c..64913f7 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,22 +1,22 @@ -import { NextFunction } from 'express'; -import JWT from 'jsonwebtoken'; +import { NextFunction } from "express"; +import JWT from "jsonwebtoken"; export const generateJWT = ( userId: number, email: string, - tokenType: 'access' | 'refresh' + tokenType: "access" | "refresh" ): string => { try { let secretVar: string; let expiresIn: string | undefined; switch (tokenType) { - case 'access': - secretVar = 'ACCESS_SECRET'; - expiresIn = '1d'; + case "access": + secretVar = "ACCESS_SECRET"; + expiresIn = "1d"; break; - case 'refresh': - secretVar = 'REFRESH_SECRET'; - expiresIn = '1y'; + case "refresh": + secretVar = "REFRESH_SECRET"; + expiresIn = "1y"; break; } @@ -24,8 +24,8 @@ export const generateJWT = ( const payload = { email }; const options = { expiresIn, - issuer: 'trustclaims.whatscookin.us', - audience: String(userId) + issuer: "trustclaims.whatscookin.us", + audience: String(userId), }; const token = JWT.sign(payload, secret, options); @@ -49,7 +49,7 @@ export const passToExpressErrorHandler = (err: any, next: NextFunction) => { if (!err.statusCode) { err.statusCode = 500; console.log(err.message); - err.message = 'Could not process the request, check inputs and try again'; + err.message = "Could not process the request, check inputs and try again"; } next(err); }; @@ -74,7 +74,7 @@ interface Mapping { // handle common mis-keys const SIMILAR_MAP: Mapping = { - howKnown: { website: 'WEB_DOCUMENT', WEBSITE: 'WEB_DOCUMENT' } + howKnown: { website: "WEB_DOCUMENT", WEBSITE: "WEB_DOCUMENT" }, }; export const poormansNormalizer = (obj: { [key: string]: any }) => { @@ -92,3 +92,13 @@ export const poormansNormalizer = (obj: { [key: string]: any }) => { export const makeClaimSubjectURL = (claimId: string) => { return `https://live.linkedtrust.us/claims/${claimId}`; }; + +export const decodeGoogleCredential = (accessToken: string) => { + const { name, email, sub } = JWT.decode(accessToken) as GoogleCredentialDecoded; + + return { + name, + email, + googleId: sub + }; +};