Skip to content

Commit

Permalink
Merge pull request #34 from atlp-rwanda/ft-google-auth-#187419170
Browse files Browse the repository at this point in the history
Ft google auth #187419170
  • Loading branch information
teerenzo authored Apr 30, 2024
2 parents 5d9a0bf + de3b324 commit 5a449cd
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 13 deletions.
34 changes: 31 additions & 3 deletions __test__/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,17 @@ describe("Testing user Routes", () => {
let token: any;
describe("Testing user authentication", () => {
test("should return 201 and create a new user when registering successfully", async () => {
const response = await request(app).post("/api/v1/users/register").send(userData);
const response = await request(app)
.post("/api/v1/users/register")
.send(userData);
expect(response.status).toBe(201);
}, 20000);

test("should return 409 when registering with an existing email", async () => {
User.create(userData);
const response = await request(app).post("/api/v1/users/register").send(userData);
const response = await request(app)
.post("/api/v1/users/register")
.send(userData);
expect(response.status).toBe(409);
}, 20000);

Expand All @@ -65,7 +69,9 @@ describe("Testing user Routes", () => {
name: "",
username: "existinguser",
};
const response = await request(app).post("/api/v1/users/register").send(userData);
const response = await request(app)
.post("/api/v1/users/register")
.send(userData);

expect(response.status).toBe(400);
}, 20000);
Expand Down Expand Up @@ -172,3 +178,25 @@ describe("Testing user Routes", () => {
expect(response.status).toBe(400);
});
});

describe("Testing user authentication", () => {
test("should return 200 when password is updated", async () => {
const response = await request(app)
.get("/login")
expect(response.status).toBe(200)
expect(response.text).toBe('<a href="/api/v1/users/auth/google"> Click to Login </a>')
});
test("should return a redirect to Google OAuth when accessing /auth/google", async () => {
const response = await request(app).get("/api/v1/users/auth/google");
expect(response.status).toBe(302);
expect(response.headers.location).toContain("https://accounts.google.com/o/oauth2");
});

test("should handle Google OAuth callback and redirect user appropriately", async () => {
const callbackFnMock = jest.fn();

const response = await request(app).get("/api/v1/users/auth/google/callback");
expect(response.status).toBe(302);
});

})
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,13 @@
"@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/nodemailer": "^6.4.14",
"@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 @@ -77,11 +80,14 @@
"dotenv": "^16.4.5",
"email-validator": "^2.0.4",
"express": "^4.19.2",
"express-session": "^1.18.0",
"husky": "^9.0.11",
"joi": "^17.13.0",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.13",
"lint-staged": "^15.2.2",
"nodemailer": "^6.9.13",
"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
32 changes: 32 additions & 0 deletions src/auth/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import passport from "passport";
import { Strategy as GoogleStrategy, Profile } from "passport-google-oauth20";
import { env } from "../utils/env";


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

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

passport.deserializeUser((user, done) => {
//@ts-ignore
done(null, user);
});
54 changes: 51 additions & 3 deletions src/controllers/userControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { generateToken } from "../utils/jsonwebtoken";
import * as mailService from "../services/mail.service";
import { IUser, STATUS, SUBJECTS } from "../types";
import { comparePasswords } from "../utils/comparePassword";
import { loggedInUser } from "../services/user.service";
import { createUserService, getUserByEmail, updateUserPassword } from "../services/user.service";
import { createUserService, getUserByEmail, updateUserPassword,loggedInUser } from "../services/user.service";
import { hashedPassword } from "../utils/hashPassword";
import Token, { TokenAttributes } from "../sequelize/models/Token";
import User from "../sequelize/models/users";
Expand Down Expand Up @@ -99,6 +98,7 @@ export const updatePassword = async (req: Request, res: Response) => {
try {
// @ts-ignore
const { user } = req;
// @ts-ignore
const isPasswordValid = await comparePasswords(oldPassword, user.password);
if (!isPasswordValid) {
return res.status(400).json({ message: "Old password is incorrect" });
Expand All @@ -107,12 +107,13 @@ export const updatePassword = async (req: Request, res: Response) => {
if (newPassword !== confirmPassword) {
return res.status(400).json({ message: "New password and confirm password do not match" });
}

// @ts-ignore
if (await comparePasswords(newPassword, user.password)) {
return res.status(400).json({ message: "New password is similar to the old one. Please use a new password" });
}

const password = await hashedPassword(newPassword);
// @ts-ignore
const update = await updateUserPassword(user, password);
if(update){
return res.status(200).json({ message: "Password updated successfully" });
Expand Down Expand Up @@ -162,3 +163,50 @@ export const tokenVerification = async (req: any, 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:IUser = await User.create({
name: user.displayName,
email: user.emails[0].value,
username: user.name.familyName,
// @ts-ignore
password: null,
});
token = await generateToken(newUser);
foundUser = newUser;
} else {
token = await generateToken(foundUser);
}

return res.status(200).json({
token: token,
message: 'success'
});
} 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,
});
}
};
3 changes: 3 additions & 0 deletions src/routes/homeRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ homeRoute.get("/", (req: Request, res: Response) => {
});
}
});
homeRoute.get("/login", (req: Request, res: Response) => {
res.send('<a href="/api/v1/users/auth/google"> Click to Login </a>')
});

export default homeRoute;
13 changes: 11 additions & 2 deletions src/routes/userRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { Router } from "express";
import { fetchAllUsers, createUserController, userLogin, updatePassword, tokenVerification } from "../controllers/userControllers";
import { fetchAllUsers, createUserController, userLogin, updatePassword, tokenVerification, handleSuccess, handleFailure } from "../controllers/userControllers";
import { emailValidation, validateSchema } from "../middleware/validator";
import signUpSchema from "../schemas/signUpSchema";
import { isLoggedIn } from "../middlewares/isLoggedIn";
import { passwordUpdateSchema } from "../schemas/passwordUpdate";
import { isTokenFound } from "../middlewares/isTokenFound";
import { authenticateUser, callbackFn } from "../services/user.service";
require("../auth/auth");

const userRoutes = Router();

userRoutes.get("/", fetchAllUsers);
userRoutes.post("/login", userLogin);
userRoutes.put("/passwordupdate", isLoggedIn, validateSchema(passwordUpdateSchema), updatePassword)
userRoutes.post('/login',userLogin);
userRoutes.post("/register", emailValidation, validateSchema(signUpSchema), createUserController);
userRoutes.put("/passwordupdate", isLoggedIn, validateSchema(passwordUpdateSchema), updatePassword);
userRoutes.post("/2fa-verify", isTokenFound, tokenVerification);

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


export default userRoutes;
2 changes: 1 addition & 1 deletion src/sequelize/migrations/20240416115110-create-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module.exports = {
},
password: {
type: Sequelize.STRING,
allowNull: false,
allowNull: true,
},
createdAt: {
allowNull: false,
Expand Down
4 changes: 2 additions & 2 deletions src/sequelize/models/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface UserAttributes {
username: string;
email: string;
role?: string[];
password: string;
password: string | undefined;
createdAt?: Date;
updatedAt?: Date;
}
Expand Down Expand Up @@ -48,7 +48,7 @@ User.init(
defaultValue: ["buyer"],
},
password: {
allowNull: false,
allowNull: true,
type: DataTypes.STRING,
},
createdAt: {
Expand Down
10 changes: 10 additions & 0 deletions src/services/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import User from "../sequelize/models/users";
import { hashedPassword } from "../utils/hashPassword";
import passport from "passport";
import { Op } from "sequelize";

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 {
const users = await User.findAll();
Expand Down
5 changes: 4 additions & 1 deletion src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ export const env = {
smtp_port: process.env.SMTP_PORT,
smtp_user: process.env.SMTP_USER as string,
smtp_password: process.env.SMTP_PASS as string,
};
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
callbackURL: process.env.GOOGLE_CALLBACK_URL as string,
};
16 changes: 16 additions & 0 deletions src/utils/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import cors from "cors";
import appROutes from "../routes";
import homeRoute from "../routes/homeRoutes";
import docRouter from "../docs/swagger";
import passport from "passport";
import session from "express-session";

const app = express();

Expand All @@ -11,6 +13,20 @@ app.use(express.urlencoded({ extended: true }));

app.use(cors());

app.use(
session({
secret: "eagles.team1",
resave: false,
saveUninitialized: false,
cookie: {
secure: false,
},
})
);

app.use(passport.initialize());
app.use(passport.session());

app.use("/", homeRoute);
app.use("/api/v1", appROutes);
app.use("/docs", docRouter);
Expand Down

0 comments on commit 5a449cd

Please sign in to comment.