Skip to content

Commit

Permalink
Feat(authentication): implementation of google authentication
Browse files Browse the repository at this point in the history
- Add signin and sign up with google account
- return access token
- added test

delivers #187419170
  • Loading branch information
MugemaneBertin2001 committed Apr 26, 2024
1 parent 12b02a5 commit b0fc4f4
Show file tree
Hide file tree
Showing 15 changed files with 198 additions and 75 deletions.
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
PORT = 3000
DB_CONNECTION = "" --> TODO: put your own connection string here
TEST_DB = ""-->TODO: put your own testing database connection string here
JWT_SECRET = ""-->TODO: put your own jsonwebtoken scret here
JWT_SECRET = ""-->TODO: put your own jsonwebtoken scret here

GOOGLE_CLIENT_ID="" use yours
GOOGLE_CLIENT_SECRET="" use yours
GOOGLE_CALLBACK_URL="" use yours
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/node_modules
/.env
/package-lock.json
/coverage
/coverage
/.vscode
14 changes: 13 additions & 1 deletion __test__/home.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,21 @@ describe("Testing Home route", () => {
}
}, 40000);

test("servr should return status code of 200 --> given'/'", async () => {
test("server should return status code of 200 --> given'/'", async () => {
const response = await request(app).get("/");

expect(response.status).toBe(200);
}, 40000);
test("server should return status code of 200 and link to log in with google --> given'/login'", async () => {
const response = await request(app).get("/login");
expect(response.text).toBe("<a href='/api/v1/users/login/google'>click to here to Login</a>")

expect(response.status).toBe(200);
}, 20000);
test("server should return status code of 200 and link to log in with google --> given'/login'", async () => {
const response = await request(app).get("/login");
expect(response.text).toBe("<a href='/api/v1/users/login/google'>click to here to Login</a>")

expect(response.status).toBe(200);
}, 20000);
});
78 changes: 12 additions & 66 deletions __test__/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import app from "../src/utils/server";
import User from "../src/sequelize/models/users";
import * as userServices from "../src/services/user.service";
import sequelize, { connect } from "../src/config/dbConnection";

const userData: any = {
name: "yvanna",
username: "testuser",
Expand Down Expand Up @@ -90,73 +89,20 @@ describe("Testing user Routes", () => {
expect(response.body.status).toBe(401);
spyonOne.mockRestore();
});
})

test("should log a user in to retrieve a token", async () => {
const response = await request(app).post("/api/v1/users/login").send({
email: userData.email,
password: userData.password,
});
expect(response.status).toBe(200);
token = response.body.token;
});

test("should return 400 when adding an extra field while updating password", async () => {
const response = await request(app)
.put("/api/v1/users/passwordupdate")
.send({
oldPassword: userData.password,
newPassword: userTestData.newPassword,
confirmPassword: userTestData.confirmPassword,
role: "seller",
})
.set("Authorization", "Bearer " + token);
expect(response.status).toBe(400);
});

test("should return 401 when updating password without authorization", async () => {
const response = await request(app)
.put("/api/v1/users/passwordupdate")
.send({
oldPassword: userData.password,
newPassword: userTestData.newPassword,
confirmPassword: userTestData.confirmPassword,
});
expect(response.status).toBe(401);
});

test("should return 200 when password is updated", async () => {
const response = await request(app)
.put("/api/v1/users/passwordupdate")
.send({
oldPassword: userData.password,
newPassword: userTestData.newPassword,
confirmPassword: userTestData.confirmPassword,
})
.set("Authorization", "Bearer " + token);
expect(response.status).toBe(200);
});
describe("Testing Google auth", () => {
test('It should return Google login page with redirect status code (302), correct headers,', async () => {
const response = await request(app).get('/api/v1/users/login/google');
expect(response.status).toBe(302);
expect(response.headers).toHaveProperty('location');
}, 20000);

test("should return 400 when confirm password and new password doesn't match", async () => {
const response = await request(app)
.put("/api/v1/users/passwordupdate")
.send({
oldPassword: userData.password,
newPassword: userTestData.newPassword,
confirmPassword: userTestData.wrongPassword,
})
.set("Authorization", "Bearer " + token);
expect(response.status).toBe(400);
test('Callback endpoint should redirect to success route after successful authentication', async () => {
const response = await request(app).get('/api/v1/users/auth/google/callback');
expect(response.status).toBe(302);
expect(response.header['location']).toContain("redirect_uri");
});

test("should return 400 when old password is incorrect", async () => {
const response = await request(app)
.put("/api/v1/users/passwordupdate")
.send({
oldPassword: userTestData.wrongPassword,
newPassword: userTestData.newPassword,
confirmPassword:userTestData.wrongPassword,
})
.set("Authorization", "Bearer " + token);
expect(response.status).toBe(400);
});
});
});
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"dev": "nodemon index.ts",
"build": "tsc",
"migrate": "npx sequelize-cli db:migrate",
"unmigrate": "npx sequelize-cli db:migrate:undo",
"seed": "npx sequelize-cli db:seed:all",
"lint:fix": "npx eslint --fix .",
"test": "cross-env NODE_ENV=test jest --detectOpenHandles --coverage",
Expand Down Expand Up @@ -42,9 +43,12 @@
"@types/cors": "^2.8.17",
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.21",
"@types/express-session": "^1.18.0",
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20.12.7",
"@types/passport": "^1.0.16",
"@types/passport-google-oauth20": "^2.0.14",
"@types/supertest": "^6.0.2",
"@types/swagger-ui-express": "^4.1.6",
"@typescript-eslint/eslint-plugin": "^7.7.1",
Expand Down Expand Up @@ -78,8 +82,11 @@
"express": "^4.19.2",
"husky": "^9.0.11",
"joi": "^17.13.0",
"express-session": "^1.18.0",
"jsonwebtoken": "^9.0.2",
"lint-staged": "^15.2.2",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"path": "^0.12.7",
"pg": "^8.11.5",
"pg-hstore": "^2.3.4",
Expand Down
30 changes: 30 additions & 0 deletions src/auth/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import passport from "passport";
import { Strategy as GoogleStrategy, Profile } from "passport-google-oauth20";
import {env} from "../utils/env";

passport.use(
new GoogleStrategy(
{
clientID: env.google_client_id,
clientSecret: env.google_client_secret,
callbackURL: env.google_redirect_url,
},
async (
accessToken: string,
refreshToken: string,
profile: Profile,
done
) => {
return done(null, profile);
}
)
);

passport.serializeUser((user, done) => {
done(null, user);
});

passport.deserializeUser((user, done) => {
//@ts-ignore
done(null, user);
});
50 changes: 49 additions & 1 deletion src/controllers/userControllers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Request, Response } from "express";
import * as userService from "../services/user.service";
import { generateToken } from "../utils/jsonwebtoken";
import { generateToken, generateUserToken } from "../utils/jsonwebtoken";
import { comparePasswords } from "../utils/comparePassword";
import { loggedInUser} from "../services/user.service";
import { createUserService, getUserByEmail, updateUserPassword } from "../services/user.service";
import User, { UserAttributes } from "../sequelize/models/users";
import { hashedPassword } from "../utils/hashPassword";

export const fetchAllUsers = async (req: Request, res: Response) => {
Expand Down Expand Up @@ -112,3 +113,50 @@ export const updatePassword = async (req: Request, res: Response) => {
})
}
}
export const handleSuccess = async (req: Request, res: Response) => {
// @ts-ignore
const user: UserProfile = req.user;

try {
let token;
let foundUser: any = await User.findOne({
where: { email: user.emails[0].value }
});

if (!foundUser) {
const newUser: UserAttributes = await User.create({
name: user.displayName,
email: user.emails[0].value,
username: user.name.familyName,
password: user.displayName,
});
token = await generateUserToken(newUser);
foundUser = newUser;
} else {
token = await generateUserToken(foundUser);
}

return res.status(200).json({
token: token,
message: 'success',
data: foundUser,
});
} catch (error: any) {
return res.status(500).json({
message: error.message,
});
}
};


export const handleFailure = async (req: Request, res: Response) => {
try {
res.status(401).json({
message: "unauthorized",
});
} catch (error: any) {
res.status(500).json({
message: error.message,
});
}
};
10 changes: 10 additions & 0 deletions src/routes/homeRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,15 @@ homeRoute.get("/", (req: Request, res: Response) => {
});
}
});
homeRoute.get("/login", (req: Request, res: Response) => {
try {
res.send("<a href='/api/v1/users/login/google'>click to here to Login</a>");
} catch (error: any) {
res.status(500).json({
message: "Internal server error",
error: error.message,
});
}
});

export default homeRoute;
16 changes: 15 additions & 1 deletion src/routes/userRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
fetchAllUsers,
createUserController,
userLogin,
updatePassword}
updatePassword, handleFailure, handleSuccess }
from "../controllers/userControllers";
import {
emailValidation,
Expand All @@ -12,6 +12,9 @@ import {
import signUpSchema from "../schemas/signUpSchema";
import { isLoggedIn } from "../middlewares/isLoggedIn";
import { passwordUpdateSchema } from "../schemas/passwordUpdate";
import * as userService from '../services/user.service'
require("../auth/auth");


const userRoutes = Router();

Expand All @@ -24,5 +27,16 @@ userRoutes.post("/register",
)
userRoutes.put("/passwordupdate", isLoggedIn, validateSchema(passwordUpdateSchema), updatePassword)

userRoutes.get("/login/google", userService.authenticateUser);
userRoutes.get("/auth/google/callback", userService.callbackFn);
userRoutes.get("/auth/google/success", handleSuccess);
userRoutes.get("/auth/google/failure", handleFailure);

userRoutes.get("/login/google", userService.authenticateUser);
userRoutes.get("/auth/google/callback", userService.callbackFn);
userRoutes.get("/auth/google/success", handleSuccess);
userRoutes.get("/auth/google/failure", handleFailure);



export default userRoutes;
1 change: 0 additions & 1 deletion src/sequelize/models/users.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Model, DataTypes } from "sequelize";
import sequelize from "../../config/dbConnection";

export interface UserAttributes {
id?: number;
name: string;
Expand Down
10 changes: 10 additions & 0 deletions src/services/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import User from "../sequelize/models/users";
import { hashedPassword } from "../utils/hashPassword";
import { Op } from "sequelize";
import passport from "passport";

export const authenticateUser = passport.authenticate("google", {
scope: ["email", "profile"],
});

export const callbackFn = passport.authenticate("google", {
successRedirect: "/api/v1/users/auth/google/success",
failureRedirect: "/api/v1/users/auth/google/failure",
});

export const getAllUsers = async () => {
try {
Expand Down
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export interface IUser {
email: string;
password: string;
isMerchant?: boolean;
createdAt: Date;
updatedAt: Date;
createdAt?: Date;
updatedAt?: Date;
}
3 changes: 3 additions & 0 deletions src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ export const env = {
db_url: process.env.DB_CONNECTION as string,
test_db_url: process.env.TEST_DB as string,
jwt_secret: process.env.JWT_SECRET,
google_client_id: process.env.GOOGLE_CLIENT_ID as string,
google_client_secret: process.env.GOOGLE_CLIENT_SECRET as string,
google_redirect_url: process.env.GOOGLE_CALLBACK_URL as string,
};
25 changes: 24 additions & 1 deletion src/utils/jsonwebtoken.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IUser } from "../types";
import { env } from "../utils/env";
import { sign, verify } from "jsonwebtoken";
import jwt,{ sign,verify } from "jsonwebtoken";

export const generateToken = async (user: IUser) => {
const accessToken = sign(
Expand All @@ -19,3 +19,26 @@ export const decodeToken = async (token: string) => {
return decoded;
}

export const generateUserToken = async (user: any) => {
try {
const token = jwt.sign({ userId: user.google_id }, "team1", {
expiresIn: "30d",
});
return token;
} catch (error: any) {
throw new Error(error.message);
}
};
export const decodeUserToken = async (token: string) => {
try {
const decoded = jwt.verify(token, "team1");
//@ts-ignore
const user = await User.findOne({ where: { google_id: decoded.userId } });
if (!user) {
throw new Error("user not found");
}
return user;
} catch (error: any) {
throw new Error(error.message);
}
};
Loading

0 comments on commit b0fc4f4

Please sign in to comment.