diff --git a/.env.example b/.env.example index fa617df9..8eac7fbd 100644 --- a/.env.example +++ b/.env.example @@ -12,3 +12,6 @@ CLIENT_URL=http://localhost:5173 IMG_HOST=http://localhost:${SERVER_PORT} SMTP_MAIL=your_smtp_mail SMTP_PASSWORD=your_smtp_password +LINKEDIN_CLIENT_ID=your_linkedin_client_id +LINKEDIN_CLIENT_SECRET=your_linkedin_client_secret +LINKEDIN_REDIRECT_URL=http://localhost:${SERVER_PORT}/api/auth/linkedin/callback diff --git a/src/app.ts b/src/app.ts index 71932ce0..eb92fff2 100644 --- a/src/app.ts +++ b/src/app.ts @@ -9,7 +9,8 @@ import adminRouter from './routes/admin/admin.route' import mentorRouter from './routes/mentor/mentor.route' import categoryRouter from './routes/category/category.route' import passport from 'passport' -import './configs/passport' +import './configs/google-passport' +import './configs/linkedin-passport' import { CLIENT_URL } from './configs/envConfig' import cookieParser from 'cookie-parser' import menteeRouter from './routes/mentee/mentee.route' diff --git a/src/configs/envConfig.ts b/src/configs/envConfig.ts index fc8ea9f2..451eebde 100644 --- a/src/configs/envConfig.ts +++ b/src/configs/envConfig.ts @@ -16,3 +16,6 @@ export const CLIENT_URL = process.env.CLIENT_URL ?? '' export const IMG_HOST = process.env.IMG_HOST ?? '' export const SMTP_MAIL = process.env.SMTP_MAIL ?? '' export const SMTP_PASS = process.env.SMTP_PASS ?? '' +export const LINKEDIN_CLIENT_ID = process.env.LINKEDIN_CLIENT_ID ?? '' +export const LINKEDIN_CLIENT_SECRET = process.env.LINKEDIN_CLIENT_SECRET ?? '' +export const LINKEDIN_REDIRECT_URL = process.env.LINKEDIN_REDIRECT_URL ?? '' diff --git a/src/configs/passport.ts b/src/configs/google-passport.ts similarity index 82% rename from src/configs/passport.ts rename to src/configs/google-passport.ts index 361b4570..8b9ab329 100644 --- a/src/configs/passport.ts +++ b/src/configs/google-passport.ts @@ -1,18 +1,18 @@ +import type { Request } from 'express' import passport from 'passport' import { Strategy as JwtStrategy } from 'passport-jwt' -import { dataSource } from './dbConfig' import Profile from '../entities/profile.entity' +import { dataSource } from './dbConfig' import { - JWT_SECRET, GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URL, - GOOGLE_CLIENT_SECRET + JWT_SECRET } from './envConfig' -import type { Request } from 'express' import { Strategy as GoogleStrategy } from 'passport-google-oauth20' import { findOrCreateUser } from '../services/auth.service' -import { type User } from '../types' +import { CreateProfile, type User } from '../types' passport.use( new GoogleStrategy( @@ -31,7 +31,14 @@ passport.use( done: (err: Error | null, user?: Profile) => void ) { try { - const user = await findOrCreateUser(profile) + const createProfile: CreateProfile = { + id: profile.id, + primary_email: profile.emails?.[0]?.value ?? '', + first_name: profile.name?.givenName ?? '', + last_name: profile.name?.familyName ?? '', + image_url: profile.photos?.[0]?.value ?? '' + } + const user = await findOrCreateUser(createProfile) done(null, user) } catch (err) { done(err as Error) diff --git a/src/configs/linkedin-passport.ts b/src/configs/linkedin-passport.ts new file mode 100644 index 00000000..2adf9875 --- /dev/null +++ b/src/configs/linkedin-passport.ts @@ -0,0 +1,100 @@ +import type { Request } from 'express' +import passport from 'passport' +import { Strategy as JwtStrategy } from 'passport-jwt' +import Profile from '../entities/profile.entity' +import { dataSource } from './dbConfig' +import { + JWT_SECRET, + LINKEDIN_CLIENT_ID, + LINKEDIN_CLIENT_SECRET, + LINKEDIN_REDIRECT_URL +} from './envConfig' + +import { Strategy as LinkedInStrategy } from 'passport-linkedin-oauth2' +import { findOrCreateUser } from '../services/auth.service' +import { CreateProfile, type User } from '../types' + +passport.use( + new LinkedInStrategy( + { + clientID: LINKEDIN_CLIENT_ID, + clientSecret: LINKEDIN_CLIENT_SECRET, + callbackURL: LINKEDIN_REDIRECT_URL, + scope: ['openid', 'email', 'profile'], + passReqToCallback: true + }, + async function ( + req: Request, + accessToken: string, + refreshToken: string, + profile: any, + done: (err: Error | null, user?: Profile) => void + ) { + try { + const createProfile: CreateProfile = { + id: profile.id, + primary_email: profile?.email ?? '', + first_name: profile?.givenName ?? '', + last_name: profile?.familyName ?? '', + image_url: profile?.picture ?? '' + } + const user = await findOrCreateUser(createProfile) + done(null, user) + } catch (err) { + done(err as Error) + } + } + ) +) + +passport.serializeUser((user: Express.User, done) => { + done(null, (user as User).primary_email) +}) + +passport.deserializeUser(async (primary_email: string, done) => { + try { + const profileRepository = dataSource.getRepository(Profile) + const user = await profileRepository.findOne({ + where: { primary_email }, + relations: ['mentor', 'mentee'] + }) + done(null, user) + } catch (err) { + done(err) + } +}) + +const cookieExtractor = (req: Request): string => { + let token = null + if (req?.cookies) { + token = req.cookies.jwt + } + return token +} + +const options = { + jwtFromRequest: cookieExtractor, + secretOrKey: JWT_SECRET +} + +passport.use( + new JwtStrategy(options, async (jwtPayload, done) => { + try { + const profileRepository = dataSource.getRepository(Profile) + const profile = await profileRepository.findOne({ + where: { uuid: jwtPayload.userId }, + relations: ['mentor', 'mentee'] + }) + + if (!profile) { + done(null, false) + } else { + done(null, profile) + } + } catch (error) { + done(error, false) + } + }) +) + +export default passport diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index d47e1995..26169e12 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -36,6 +36,30 @@ export const googleRedirect = async ( )(req, res, next) } +export const linkedinRedirect = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { + passport.authenticate( + 'linkedin', + { failureRedirect: '/login' }, + (err: Error, user: Profile) => { + if (err) { + next(err) + return + } + if (!user) { + res.redirect('/login') + return + } + signAndSetCookie(res, user.uuid) + + res.redirect(process.env.CLIENT_URL ?? '/') + } + )(req, res, next) +} + export const register = async ( req: Request, res: Response diff --git a/src/routes/auth/auth.route.ts b/src/routes/auth/auth.route.ts index 6cab0e3d..e3c6c712 100644 --- a/src/routes/auth/auth.route.ts +++ b/src/routes/auth/auth.route.ts @@ -1,13 +1,14 @@ import express from 'express' +import passport from 'passport' import { - register, + googleRedirect, + linkedinRedirect, login, logout, - googleRedirect, + passwordReset, passwordResetRequest, - passwordReset + register } from '../../controllers/auth.controller' -import passport from 'passport' const authRouter = express.Router() @@ -22,7 +23,15 @@ authRouter.get( }) ) +authRouter.get( + '/linkedin', + passport.authenticate('linkedin', { + scope: ['openid', 'email', 'profile'] + }) +) + authRouter.get('/google/callback', googleRedirect) +authRouter.get('/linkedin/callback', linkedinRedirect) authRouter.post('/password-reset-request', passwordResetRequest) authRouter.put('/passwordreset', passwordReset) export default authRouter diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 8f2e2bc3..54a58261 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,15 +1,14 @@ -import { dataSource } from '../configs/dbConfig' import bcrypt from 'bcrypt' -import Profile from '../entities/profile.entity' -import type passport from 'passport' import jwt from 'jsonwebtoken' +import { dataSource } from '../configs/dbConfig' import { JWT_SECRET } from '../configs/envConfig' +import Profile from '../entities/profile.entity' +import { CreateProfile, type ApiResponse } from '../types' import { - getPasswordResetEmailContent, - getPasswordChangedEmailContent + getPasswordChangedEmailContent, + getPasswordResetEmailContent } from '../utils' import { sendResetPasswordEmail } from './admin/email.service' -import { type ApiResponse } from '../types' export const registerUser = async ( email: string, @@ -88,21 +87,22 @@ export const loginUser = async ( } export const findOrCreateUser = async ( - profile: passport.Profile + createProfileDto: CreateProfile ): Promise => { const profileRepository = dataSource.getRepository(Profile) + let user = await profileRepository.findOne({ - where: { primary_email: profile.emails?.[0]?.value ?? '' }, - relations: ['mentor', 'mentee'] + where: { primary_email: createProfileDto.primary_email } }) + if (!user) { - const hashedPassword = await bcrypt.hash(profile.id, 10) // Use Google ID as password + const hashedPassword = await bcrypt.hash(createProfileDto.id, 10) user = profileRepository.create({ - primary_email: profile.emails?.[0]?.value ?? '', + primary_email: createProfileDto.primary_email, password: hashedPassword, - first_name: profile.name?.givenName, - last_name: profile.name?.familyName, - image_url: profile.photos?.[0]?.value ?? '' + first_name: createProfileDto.first_name, + last_name: createProfileDto.last_name, + image_url: createProfileDto.image_url }) await profileRepository.save(user) } diff --git a/src/types.ts b/src/types.ts index 4a6353e7..44afef65 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,3 +26,11 @@ export interface ApiResponse { export interface User extends Express.User { primary_email: string } + +export interface CreateProfile { + primary_email: string + id: string + first_name: string + last_name: string + image_url: string +}