diff --git a/.env.example b/.env.example deleted file mode 100644 index 0a48e9f..0000000 --- a/.env.example +++ /dev/null @@ -1,13 +0,0 @@ -PORT= ******************************** -APP_ENV= ******************************** -PDN_DB_NAME= ***************************** -DEV_DB_HOST= ******************************** -DEV_DB_PORT= ******************************** -DEV_DB_USER= ******************************** -DEV_DB_PASS= ***************************** -DEV_DB_TYPE= ******************************* - -PDN_DB_HOST= ******************************** -PDN_DB_PORT= ******************************** -PDN_DB_USER= ******************************** -PDN_DB_PASS= ******************************** \ No newline at end of file diff --git a/package.json b/package.json index 84a5b11..a045ce1 100644 --- a/package.json +++ b/package.json @@ -26,11 +26,16 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-session": "^1.18.0", "express-winston": "^4.2.0", "highlight.js": "^11.9.0", "jsend": "^1.1.0", "morgan": "^1.10.0", "nodemon": "^3.1.0", + "passport": "^0.7.0", + "passport-facebook": "^3.0.0", + "passport-google-oauth": "^2.0.0", + "passport-google-oauth20": "^2.0.0", "pg": "^8.11.5", "reflect-metadata": "^0.2.2", "source-map-support": "^0.5.21", @@ -53,10 +58,14 @@ "@types/eslint": "^8.56.10", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", "@types/jest": "^29.5.12", "@types/jsend": "^1.0.32", "@types/morgan": "^1.9.9", "@types/node": "^20.12.7", + "@types/passport": "^1.0.16", + "@types/passport-facebook": "^3.0.3", + "@types/passport-google-oauth20": "^2.0.14", "@types/reflect-metadata": "^0.1.0", "@types/supertest": "^6.0.2", "@types/winston": "^2.4.4", @@ -75,4 +84,4 @@ "typescript": "^5.4.5", "typescript-eslint": "^7.7.1" } -} \ No newline at end of file +} diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 209a350..a98cece 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -1,5 +1,5 @@ import { Request, Response } from 'express'; -import { User } from '../entities/User';; +import { User } from '../entities/User'; import bcrypt from 'bcrypt'; import { getRepository } from 'typeorm'; diff --git a/src/controllers/facebook.auth.ts b/src/controllers/facebook.auth.ts new file mode 100644 index 0000000..783febe --- /dev/null +++ b/src/controllers/facebook.auth.ts @@ -0,0 +1,82 @@ +import { getRepository } from 'typeorm'; +import { User } from '../entities/user.auth'; +import express from 'express'; +import passport from 'passport'; +// import session from 'express-session'; +// import { PassportStatic } from 'passport'; +const routerfb = express.Router(); +require('dotenv').config(); + +async function findUserAccountIdProvider(accountId: string, provider: string): Promise { + try { + const userRepository = getRepository(User); + + const user = await userRepository.findOne({ + where: { + accountId: accountId, + provider: provider, + }, + }); + + if (user === null) { + return undefined; + } + + return user as User; + } catch (error) { + console.error('Error finding user:', error); + return undefined; + } + } +const accountId = 'your_account_id'; +const provider = 'facebook'; + +findUserAccountIdProvider(accountId, provider) + .then((user) => { + if (user) { + console.log('User found:', user); + } else { + console.log('User not found'); + } + }) + .catch((error) => { + console.error('Error:', error); + }); + + routerfb.get('/', passport.authenticate('facebook', { scope: 'email' })); + + routerfb.get( + '/callback', + passport.authenticate('facebook', { + failureRedirect: '/auth/facebook/error', + }), + function (req, res) { + // Successful authentication, redirect to success screen. + res.redirect('/auth/facebook/success'); + } + ); + + routerfb.get('/success', async (req, res) => { + const userInfo = { + id: req.session.passport.user.id, + displayName: req.session.passport.user.displayName, + provider: req.session.passport.user.provider, + }; + res.render('fb-github-success', { user: userInfo }); + }); + + routerfb.get('/error', (req, res) => res.send('Error logging in via Facebook..')); + + routerfb.get('/signout', (req, res) => { + try { + req.session.destroy(function (err) { + console.log('session destroyed.'); + }); + res.render('auth'); + } catch (err) { + res.status(400).send({ message: 'Failed to sign out fb user' }); + } + }); + + module.exports = routerfb; + diff --git a/src/controllers/google.auth.ts b/src/controllers/google.auth.ts new file mode 100644 index 0000000..4cba5a2 --- /dev/null +++ b/src/controllers/google.auth.ts @@ -0,0 +1,49 @@ +import passport from 'passport'; +const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; +import express from 'express'; +import googleAuth from '../middlewares/auth'; +const routers = express.Router(); +require('dotenv').config(); + +let userProfile: any; +passport.use( + new GoogleStrategy( + { + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: process.env.CALLBACK_URL, + }, + function (_accessToken: any, _refreshToken: any, profile: any, done: (arg0: null, arg1: any) => any) { + userProfile = profile; + return done(null, userProfile); + } + ) +); + +// request at /auth/google, when user click sign-up with google button transferring +// the request to google server, to show emails screen +routers.get( + '/', + passport.authenticate('google', { scope: ['profile', 'email'] }) +); + +// URL Must be same as 'Authorized redirect URIs' field of OAuth client, i.e: /auth/google/callback +routers.get( + '/callback', + passport.authenticate('google', { failureRedirect: '/auth/google/error' }), + (req: any, res: { redirect: (arg0: string) => void; }) => { + res.redirect('/auth/google/success'); // Successful authentication, redirect success. + } +); + +routers.get('/success', async (req: any, res: { render: (arg0: string, arg1: { user: any; }) => void; }) => { + const { failure, success } = await googleAuth.registerWithGoogle(userProfile); + if (failure) console.log('Google user already exist in DB..'); + else console.log('Registering new Google user..'); + res.render('success', { user: userProfile }); +}); + +routers.get('/error', (req: any, res: { send: (arg0: string) => any; }) => res.send('Error logging in via Google..')); + + +export default routers; \ No newline at end of file diff --git a/src/entities/User.ts b/src/entities/User.ts index 2d0557f..2ab958c 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -11,6 +11,15 @@ import { @Entity() @Unique(['email']) export class User { + save() { + throw new Error('Method not implemented.'); + } + static create(arg0: { accountId: string; name: string; provider: string; }): User | PromiseLike | null { + throw new Error('Method not implemented.'); + } + static findOne(arg0: { accountId: any; provider: string; }) { + throw new Error('Method not implemented.'); + } @PrimaryGeneratedColumn('uuid') @IsNotEmpty() id!: string; @@ -66,4 +75,20 @@ import { @UpdateDateColumn() updatedAt!: Date; + + @Column({ nullable: true }) + @IsString() + accountId?: string; + + @Column({ nullable: true }) + @IsString() + name?: string; + + @Column({ nullable: true }) + @IsString() + photoURL?: string; + + @Column({ nullable: true }) + @IsString() + provider?: string; } \ No newline at end of file diff --git a/src/entities/user.auth.ts b/src/entities/user.auth.ts new file mode 100644 index 0000000..d8270bb --- /dev/null +++ b/src/entities/user.auth.ts @@ -0,0 +1,22 @@ +import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm'; + +@Entity() +export class User extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id: string | undefined; + + @Column({ type: 'text', nullable: true, unique: true }) + email: string | undefined; + + @Column({ type: 'text', nullable: true }) + accountId: string | undefined; + + @Column({ type: 'text', nullable: true }) + name!: string; + + @Column({ type: 'text', nullable: true }) + photoURL: string | undefined; + + @Column({ type: 'text', nullable: true }) + provider: string | undefined; +} diff --git a/src/index.ts b/src/index.ts index 55443f6..44283f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,16 @@ import express, { Request, Response } from 'express'; import cors from 'cors'; -import dotenv from 'dotenv'; import router from './routes'; import { addDocumentation } from './startups/docs'; import 'reflect-metadata'; - - +import dotenv from 'dotenv'; +dotenv.config(); import { CustomError, errorHandler } from './middlewares/errorHandler'; import morgan from 'morgan'; import { dbConnection } from './startups/dbConnection'; +import passport from 'passport'; +import session from 'express-session'; +import routers from '../src/controllers/google.auth'; dotenv.config(); export const app = express(); @@ -32,6 +34,13 @@ dbConnection(); const morganFormat = ':method :url :status :response-time ms - :res[content-length]'; app.use(morgan(morganFormat)); + +//Google OAuth routes +app.use('/auth/google', routers); export const server = app.listen(port, () => { console.log(`[server]: Server is running at http://localhost:${port}`); -}); \ No newline at end of file +}); + +function passportConfig() { + throw new Error('Function not implemented.'); +} diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts new file mode 100644 index 0000000..590c68a --- /dev/null +++ b/src/middlewares/auth.ts @@ -0,0 +1,72 @@ +import { User } from '../entities/user.auth'; +import { getRepository, FindOneOptions } from 'typeorm'; + +interface OAuthUser { + id: any; + provider: any; + displayName: any; + emails: { value: any }[]; + photos: { value: any }[]; +} + +const googleAuthDal = { + registerWithGoogle: async (oauthUser: OAuthUser) => { + const userRepository = getRepository(User); + + try { + // Check if user already exists + const existingUser = await userRepository.findOne({ + where: { + accountId: oauthUser.id, + provider: oauthUser.provider, + }, + }); + + if (existingUser) { + return { failure: { message: 'User already registered.' } }; + } + + // Create a new user entity + const newUser = userRepository.create({ + accountId: oauthUser.id, + name: oauthUser.displayName, + provider: oauthUser.provider, + // Adjust property names based on your User entity + email: oauthUser.emails[0]?.value, // Assuming emails[0] contains the email value + photoURL: oauthUser.photos[0]?.value, // Assuming photos[0] contains the photo URL value + }); + + // Save the new user to the database + await userRepository.save(newUser); + + return { success: { message: 'User registered successfully.' } }; + } catch (error) { + console.error('Error registering user with Google:', error); + return { failure: { message: 'Failed to register user.' } }; + } + }, + + loginUser: async (oauthUser: OAuthUser) => { + const userRepository = getRepository(User); + + try { + // Check if a user with the provided email exists + const userExists = await userRepository.findOne({ + where: { + email: oauthUser.emails[0]?.value, // Assuming emails[0] contains the email value + }, + }); + + if (userExists) { + return { success: { message: 'User successfully logged in.' } }; + } else { + return { failure: { message: 'Email not registered. You need to sign up first.' } }; + } + } catch (error) { + console.error('Error logging in user:', error); + return { failure: { message: 'Failed to log in user.' } }; + } + }, +}; + +export default googleAuthDal; diff --git a/src/types/session.d.ts b/src/types/session.d.ts new file mode 100644 index 0000000..c9cb6a8 --- /dev/null +++ b/src/types/session.d.ts @@ -0,0 +1,8 @@ +import 'express-session'; +import { PassportStatic } from 'passport'; + +declare module 'express-session' { + interface SessionData { + passport?: any; // Define the passport property as any + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 6e7652b..5a1f55e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,8 +25,10 @@ "module": "commonjs" /* Specify what module code is generated. */, "rootDir": "./src" /* Specify the root folder within your source files. */, // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + "paths": { + "*": ["types/*"] + }, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ "types": ["node", "jest"] /* Specify type package names to be included without being referenced in a source file. */, @@ -98,6 +100,6 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "types"], "exclude": ["node_modules"] }