diff --git a/package.json b/package.json index 84a5b11..5c7a4d3 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@types/express": "^4.17.21", "@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/reflect-metadata": "^0.1.0", @@ -69,10 +70,11 @@ "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", "jest-mock-extended": "^3.0.6", + "jsonwebtoken": "^9.0.2", "prettier": "^3.2.5", "supertest": "^7.0.0", "ts-jest": "^29.1.2", "typescript": "^5.4.5", "typescript-eslint": "^7.7.1" } -} \ No newline at end of file +} diff --git a/src/__test__/signin.test.ts b/src/__test__/signin.test.ts new file mode 100644 index 0000000..5930d54 --- /dev/null +++ b/src/__test__/signin.test.ts @@ -0,0 +1,195 @@ +import request from 'supertest'; +import { app, server } from '../index'; // update this with the path to your app file +import { Any, createConnection, getConnection, getConnectionOptions, getRepository } from 'typeorm'; +import { User } from '../entities/User'; + +beforeAll(async () => { + // Connect to the test database + const connectionOptions = await getConnectionOptions(); + await createConnection({ ...connectionOptions, name: 'testConnection' }); +}); + +afterAll(async () => { + await getConnection('testConnection').close(); + server.close(); +}); + +describe('POST /user/login', () => { + it('should log in a user with email and password', async () => { + // sign up a user + const registerUser = { + firstName: 'Ndevu', + lastName: 'Elisa', + email: 'ndevukumurindi@gmail.com', + gender: 'male', + phoneNumber: '078907987443', + photoUrl: 'https://example.com/images/photo.jpg', + userType: 'vender', + verified: true, + status: 'active', + password: 'ndevurefu', + }; + await request(app).post('/user/register').send(registerUser); + + // Arrange + const loginUser = { + email: 'ndevukumurindi@gmail.com', + password: 'ndevurefu', + }; + const res = await request(app).post('/user/login').send(loginUser); + expect(res.status).toBe(200); + expect(res.body).toEqual({ + status: 'success', + data: { + code: 200, + message: 'logged in successful', + data: expect.any(String), + }, + }); + + // Clean up: delete the test user + const userRepository = getRepository(User); + const user = await userRepository.findOne({ where: { email: registerUser.email } }); + if (user) { + await userRepository.remove(user); + } + }); + + it('should not log in a user with empty credentials', async () => { + // Arrange + const loginUser = { + email: '', + password: '', + }; + const res = await request(app).post('/user/login').send(loginUser); + expect(res.status).toBe(400); + expect(res.body).toEqual({ Message: 'Email and password are required' }); + }); + + it('should not log in a user with wrong email', async () => { + // sign up a user + const registerUser = { + firstName: 'Ndevu', + lastName: 'Elisa', + email: 'ndevukkkk@gmail.com', + gender: 'male', + phoneNumber: '0789044308', + photoUrl: 'https://example.com/images/photo.jpg', + userType: 'vender', + verified: true, + status: 'active', + password: 'ndevu1', + }; + await request(app).post('/user/register').send(registerUser); + // Arrange + const loginUser = { + email: 'ndevuk@gmail.com', + password: 'ndevu1', + }; + const res = await request(app).post('/user/login').send(loginUser); + expect(res.status).toBe(400); + expect(res.body).toEqual({ Message: 'Invalid email' }); + + // Clean up: delete the test user + const userRepository = getRepository(User); + const user = await userRepository.findOne({ where: { email: registerUser.email } }); + if (user) { + await userRepository.remove(user); + } + }); + + it('should not log in a user with unverified email', async () => { + // sign up a user + const registerUser = { + firstName: 'Ndevu', + lastName: 'Elisa', + email: 'ndevumnu@gmail.com', + gender: 'male', + phoneNumber: '0789044399', + photoUrl: 'https://example.com/images/photo.jpg', + userType: 'vender', + verified: 'false', + status: 'active', + password: 'ndevu2', + }; + await request(app).post('/user/register').send(registerUser); + // Arrange + const loginUser = { + email: 'ndevumnu@gmail.com', + password: 'ndevu2', + }; + const res = await request(app).post('/user/login').send(loginUser); + expect(res.status).toBe(400); + expect(res.body).toEqual({ Message: 'Email not verified. verified it first' }); + // Clean up: delete the test user + const userRepository = getRepository(User); + const user = await userRepository.findOne({ where: { email: registerUser.email } }); + if (user) { + await userRepository.remove(user); + } + }); + + it('should not log in a user with suspended account', async () => { + // sign up a user + const registerUser = { + firstName: 'Ndevu', + lastName: 'Elisa', + email: 'ndevutest1@gmail.com', + gender: 'male', + phoneNumber: '0789044391', + photoUrl: 'https://example.com/images/photo.jpg', + userType: 'vender', + verified: true, + status: 'suspended', + password: 'ndevu3', + }; + await request(app).post('/user/register').send(registerUser); + // Arrange + const loginUser = { + email: 'ndevutest1@gmail.com', + password: 'ndevu3', + }; + const res = await request(app).post('/user/login').send(loginUser); + expect(res.status).toBe(400); + expect(res.body).toEqual({ Message: 'You have been suspended, reach customer service for more details' }); + + // Clean up: delete the test user + const userRepository = getRepository(User); + const user = await userRepository.findOne({ where: { email: registerUser.email } }); + if (user) { + await userRepository.remove(user); + } + }); + + it('should not log in a user with wrong password', async () => { + // sign up a user + const registerUser = { + firstName: 'Ndevu', + lastName: 'Elisa', + email: 'ndevumunene@gmail.com', + gender: 'male', + phoneNumber: '0709044398', + photoUrl: 'https://example.com/images/photo.jpg', + userType: 'vender', + verified: true, + status: 'active', + password: 'ndevu4', + }; + await request(app).post('/user/register').send(registerUser); + // Arrange + const loginUser = { + email: 'ndevumunene@gmail.com', + password: 'ndevu123', + }; + const res = await request(app).post('/user/login').send(loginUser); + expect(res.status).toBe(400); + expect(res.body).toEqual({ Message: 'Invalid password' }); + + // Clean up: delete the test user + const userRepository = getRepository(User); + const user = await userRepository.findOne({ where: { email: registerUser.email } }); + if (user) { + await userRepository.remove(user); + } + }); +}); diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 554dd4e..213eaeb 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,3 +1,4 @@ import { UserController } from './authController'; +import { login } from './loginAndOutController'; -export{UserController}; \ No newline at end of file +export { UserController, login }; diff --git a/src/controllers/loginAndOutController.ts b/src/controllers/loginAndOutController.ts new file mode 100644 index 0000000..eabd913 --- /dev/null +++ b/src/controllers/loginAndOutController.ts @@ -0,0 +1,58 @@ +import { Request, Response } from 'express'; +import { User } from '../entities/User'; +import { responseSuccess } from '../utils/response.utils'; +import { getRepository } from 'typeorm'; +import { tokenize, check } from '../helpers/TokenizeAndVerifyPass'; + +// Method to login admin +export const login = async (req: Request, res: Response): Promise => { + try { + const { email, password } = req.body; + + if (!email || !password) { + res.status(400).json({ Message: 'Email and password are required' }); + return; + } + + const getrepository = getRepository(User); + const user = await getrepository.findOneBy({ email: email }); + + if (!user) { + res.status(400).json({ Message: 'Invalid email' }); + return; + } + + if (user.verified !== true) { + res.status(400).json({ Message: 'Email not verified. verified it first' }); + return; + } + + if (user.status !== 'active') { + res.status(400).json({ Message: 'You have been suspended, reach customer service for more details' }); + return; + } + + const isPasswordValid = await check(user.password, password); + if (!isPasswordValid) { + res.status(400).json({ Message: 'Invalid password' }); + return; + } + + const accessToken = tokenize({ + id: user.id, + email: user.email, + role: user.userType, + }); + + if (process.env.NODE_ENV === 'production') { + res.cookie('token', accessToken, { httpOnly: true, sameSite: false, secure: true }); + } else { + res.cookie('token', accessToken, { httpOnly: true, sameSite: 'lax', secure: false }); + } + + responseSuccess(res, 200, 'logged in successful', accessToken); + } catch (error) { + console.error('Error logging in a user:', error); + res.status(500).json({ error: 'Sorry, Something went wrong' }); + } +}; diff --git a/src/helpers/TokenizeAndVerifyPass.ts b/src/helpers/TokenizeAndVerifyPass.ts new file mode 100644 index 0000000..a6a57d2 --- /dev/null +++ b/src/helpers/TokenizeAndVerifyPass.ts @@ -0,0 +1,18 @@ +import jwt from 'jsonwebtoken'; +import dotenv from 'dotenv'; +import bcrypt from 'bcrypt'; + +dotenv.config(); + +const jwtSecretKey = process.env.JWT_SECRETKEY; + +if (!jwtSecretKey) { + throw new Error('JWT_SECRETKEY is not defined in the environment variables.'); +} + +export const tokenize = (payload: string | object | Buffer): string => + jwt.sign(payload, jwtSecretKey, { expiresIn: '48h' }); + +export const check = (hashedPassword: any, password: string): boolean => { + return bcrypt.compareSync(password, hashedPassword); +}; diff --git a/src/routes/UserRoutes.ts b/src/routes/UserRoutes.ts index 734a565..167b578 100644 --- a/src/routes/UserRoutes.ts +++ b/src/routes/UserRoutes.ts @@ -1,11 +1,12 @@ -import { Router } from 'express'; +import { Router } from 'express'; import { UserController } from '../controllers/index'; - +import { login } from '../controllers/index'; const { registerUser } = UserController; const router = Router(); router.post('/register', registerUser); +router.post('/login', login); -export default router; \ No newline at end of file +export default router;