Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ CLOUDINARY_CLOUD_NAME=your_cloudinary_cloud_name
CLOUDINARY_API_KEY=your_cloudinary_api_key
CLOUDINARY_API_SECRET=your_cloudinary_api_secret

GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret_key
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret_key
# Google recaptcha secret key
RECAPTCHA_SECRET_KEY=your_recaptcha_secret_key

Expand Down
83 changes: 83 additions & 0 deletions backend/lib/passport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import passport from 'passport'
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'
import { Strategy as GitHubStrategy } from 'passport-github2'
import User from '../models/User.js'

passport.use(new GoogleStrategy(

Check failure on line 6 in backend/lib/passport.js

View workflow job for this annotation

GitHub Actions / test (20.x)

tests/user.test.js

TypeError: OAuth2Strategy requires a clientID option ❯ Strategy.OAuth2Strategy node_modules/passport-oauth2/lib/strategy.js:87:34 ❯ new Strategy node_modules/passport-google-oauth20/lib/strategy.js:52:18 ❯ lib/passport.js:6:14 ❯ server.js:3:1

Check failure on line 6 in backend/lib/passport.js

View workflow job for this annotation

GitHub Actions / test (20.x)

tests/passwordReset.test.js

TypeError: OAuth2Strategy requires a clientID option ❯ Strategy.OAuth2Strategy node_modules/passport-oauth2/lib/strategy.js:87:34 ❯ new Strategy node_modules/passport-google-oauth20/lib/strategy.js:52:18 ❯ lib/passport.js:6:14 ❯ server.js:3:1

Check failure on line 6 in backend/lib/passport.js

View workflow job for this annotation

GitHub Actions / test (20.x)

tests/otp.test.js

TypeError: OAuth2Strategy requires a clientID option ❯ Strategy.OAuth2Strategy node_modules/passport-oauth2/lib/strategy.js:87:34 ❯ new Strategy node_modules/passport-google-oauth20/lib/strategy.js:52:18 ❯ lib/passport.js:6:14 ❯ server.js:3:1

Check failure on line 6 in backend/lib/passport.js

View workflow job for this annotation

GitHub Actions / test (20.x)

tests/message.test.js

TypeError: OAuth2Strategy requires a clientID option ❯ Strategy.OAuth2Strategy node_modules/passport-oauth2/lib/strategy.js:87:34 ❯ new Strategy node_modules/passport-google-oauth20/lib/strategy.js:52:18 ❯ lib/passport.js:6:14 ❯ server.js:3:1

Check failure on line 6 in backend/lib/passport.js

View workflow job for this annotation

GitHub Actions / test (20.x)

tests/integration.test.js

TypeError: OAuth2Strategy requires a clientID option ❯ Strategy.OAuth2Strategy node_modules/passport-oauth2/lib/strategy.js:87:34 ❯ new Strategy node_modules/passport-google-oauth20/lib/strategy.js:52:18 ❯ lib/passport.js:6:14 ❯ server.js:3:1

Check failure on line 6 in backend/lib/passport.js

View workflow job for this annotation

GitHub Actions / test (20.x)

tests/auth.test.js

TypeError: OAuth2Strategy requires a clientID option ❯ Strategy.OAuth2Strategy node_modules/passport-oauth2/lib/strategy.js:87:34 ❯ new Strategy node_modules/passport-google-oauth20/lib/strategy.js:52:18 ❯ lib/passport.js:6:14 ❯ server.js:3:1
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: `${process.env.BACKEND_URL}/api/v1/auth/google/callback`,
},
async (_accessToken, _refreshToken, profile, done) => {
try {

let user = await User.findOne({ googleId: profile.id })


if (!user) {
const email = profile.emails?.[0]?.value
user = email ? await User.findOne({ email }) : null

if (user) {

user.googleId = profile.id
if (!user.avatarUrl) user.avatarUrl = profile.photos?.[0]?.value
await user.save()
} else {

user = await User.create({
googleId: profile.id,
email: profile.emails?.[0]?.value,
name: profile.displayName || profile.emails?.[0]?.value?.split('@')[0] || 'User',
avatarUrl: profile.photos?.[0]?.value,
password: null,
})
}
}

done(null, user)
} catch (err) {
done(err, null)
}
}
))

passport.use(new GitHubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: `${process.env.BACKEND_URL}/api/v1/auth/github/callback`,
scope: ['user:email'],
},
async (_accessToken, _refreshToken, profile, done) => {
try {
let user = await User.findOne({ githubId: profile.id })

if (!user) {
const email = profile.emails?.[0]?.value
user = email ? await User.findOne({ email }) : null

if (user) {
user.githubId = profile.id
if (!user.avatarUrl) user.avatarUrl = profile.photos?.[0]?.value
await user.save()
} else {
user = await User.create({
githubId: profile.id,
email: profile.emails?.[0]?.value,
name: profile.displayName || profile.username || profile.emails?.[0]?.value?.split('@')[0] || 'User',
avatarUrl: profile.photos?.[0]?.value,
password: null,
})
}
}

done(null, user)
} catch (err) {
done(err, null)
}
}
))

export default passport
13 changes: 12 additions & 1 deletion backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ const userSchema = new mongoose.Schema(

password: {
type: String,
required: true,
required: false,
minlength: 6,
default: null,
},

name: {
Expand Down Expand Up @@ -55,6 +56,16 @@ const userSchema = new mongoose.Schema(
type: Date,
default: null,
},
googleId: {
type: String,
default: null,
index: true,
},
githubId: {
type: String,
default: null,
index: true,
},
},
{ timestamps: true },
)
Expand Down
98 changes: 98 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
"mongoose": "^8.16.1",
"nodemailer": "^8.0.7",
"nodemon": "^3.1.10",
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
"passport-google-oauth20": "^2.0.0",
"socket.io": "^4.8.1",
"swagger-jsdoc": "^6.3.0",
"swagger-ui-express": "^5.0.1",
Expand Down
45 changes: 45 additions & 0 deletions backend/routes/auth.oauth.routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import express from 'express'
import passport from '../lib/passport.js'
import { generateToken } from '../lib/utils.js'

const oauthRouter = express.Router()

// Google

oauthRouter.get(
'/google',
passport.authenticate('google', { scope: ['profile', 'email'], session: false })
)

oauthRouter.get(
'/google/callback',
passport.authenticate('google', {
session: false,
failureRedirect: `${process.env.CLIENT_URL}/login?error=oauth_failed`,
}),
(req, res) => {
const token = generateToken(req.user._id)
// Redirect to frontend callback page with token in query param
res.redirect(`${process.env.CLIENT_URL}/oauth-callback?token=${token}`)
}
)

// GitHub
oauthRouter.get(
'/github',
passport.authenticate('github', { scope: ['user:email'], session: false })
)

oauthRouter.get(
'/github/callback',
passport.authenticate('github', {
session: false,
failureRedirect: `${process.env.CLIENT_URL}/login?error=oauth_failed`,
}),
(req, res) => {
const token = generateToken(req.user._id)
res.redirect(`${process.env.CLIENT_URL}/oauth-callback?token=${token}`)
}
)

export default oauthRouter
10 changes: 7 additions & 3 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import express from "express"
import "dotenv/config"

import passport from './lib/passport.js'
import oauthRouter from './routes/auth.oauth.routes.js'
import express from "express"

import cors from "cors"
import http from "http"
import helmet from "helmet"
Expand Down Expand Up @@ -198,7 +202,7 @@ io.on("connection", async (socket) => {
})

app.use(express.json({ limit: "4mb" }))

app.use(passport.initialize())
// 4. Routes with Rate Limiting applied
app.use("/api/status", (req, res) => {
res.status(200).json({ status: "ok" })
Expand All @@ -220,8 +224,8 @@ app.use("/api/health", (req, res) => {

// Apply strict limiter to auth routes, and standard limiter to message routes
app.use("/api/v1/auth", authLimiter, userRouter)
app.use("/api/v1/auth", authLimiter, oauthRouter)
app.use("/api/v1/messages", standardLimiter, messageRouter)

app.use("/", (req, res) => {
res.send("NPMChat API is running")
})
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use client"
import OAuthButtons from '@/components/OAuthButtons'
import React, { useState } from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
Expand Down Expand Up @@ -139,7 +140,7 @@ function LoginPageContent() {
className="mt-2 border-2 border-black bg-[#b39ddb] text-black font-extrabold text-lg py-2 rounded-none transition-all cursor-[url('/custom-cursor-click.svg'),_pointer] hover:bg-[#39ff14] hover:text-white focus:outline-none"
style={{ boxShadow: `4px 4px 0 0 ${accent}` }}
>
{loading ? "Logging In..." : "Login 2"}
{loading ? "Logging In..." : "Login"}
</button>
<div className="text-center mt-2">
<Link
Expand All @@ -149,6 +150,7 @@ function LoginPageContent() {
Don't have an account? Sign up
</Link>
</div>
<OAuthButtons label="Log in" />
<div className="text-center">
<Link
href="/forgot-password"
Expand All @@ -158,6 +160,7 @@ function LoginPageContent() {
</Link>
</div>
</form>

{/* Floating accent shape bottom left */}
<div className="absolute bottom-0 left-0 w-32 h-32 bg-[#b39ddb] border-2 border-black rotate-12 opacity-50 z-0"></div>
</main>
Expand Down
Loading
Loading