diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 00000000..9d7d41cd --- /dev/null +++ b/.github/README.md @@ -0,0 +1,56 @@ +# GitHub Actions CI/CD + +This directory contains the GitHub Actions workflows for the Codify project. + +## Workflows + +### 1. CI Pipeline (`ci.yml`) +- **Trigger**: Pull requests and pushes to main branch +- **Jobs**: + - **Frontend**: Tests the React/Vite client application + - **Backend**: Tests the Node.js/Express server application + +### 2. Issue Automation (`issue-create-automate-message.yml`) +- **Trigger**: When new issues are created +- **Purpose**: Automatically comments on new issues with helpful information + +### 3. PR Automation (`pr-create-automate-message.yml`) +- **Trigger**: When new pull requests are opened +- **Purpose**: Automatically comments on new PRs with review guidelines + +### 4. Test Build (`test-build.yml`) +- **Trigger**: Manual workflow dispatch +- **Purpose**: Manual testing of build processes + +## Common Issues and Solutions + +### Node.js Version +- All workflows use Node.js 18 for consistency +- Ensure your local development uses the same version + +### Dependencies +- Frontend dependencies are installed from `./client/package.json` +- Backend dependencies are installed from `./server/package.json` +- Root `package.json` should not have conflicting dependencies + +### Environment Variables +- CI uses dummy values for external APIs during testing +- Real environment variables should be configured in repository secrets + +### Health Checks +- Frontend health check hits `http://localhost:5173` +- Backend health check hits `http://localhost:5050` +- Both use 20-second startup delays to ensure services are ready + +## Troubleshooting + +1. **Build Failures**: Check Node.js version compatibility +2. **Dependency Issues**: Ensure package.json files are properly configured +3. **Health Check Failures**: Verify port numbers and startup times +4. **Action Version Issues**: Keep GitHub Actions versions updated + +## Maintenance + +- Review and update GitHub Actions versions quarterly +- Test CI pipeline changes in feature branches +- Monitor workflow run times and optimize if needed \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4985770..6ff63ce4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,60 +5,79 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5.0.0 - with: - path: . + uses: actions/checkout@v4 - name: Setup Node - uses: actions/setup-node@v5.0.0 - - name: Install Dependencies + uses: actions/setup-node@v4 + with: + node-version: '18' + - name: Install Frontend Dependencies + working-directory: ./client run: npm install + - name: Build Frontend + working-directory: ./client + env: + VITE_SERVER_API: http://localhost:5050 + VITE_YOUTUBE_API: dummy + VITE_GITHUB_TOKEN: dummy + VITE_RAPIDAPI_KEY: dummy + run: npm run build - name: Check Health + working-directory: ./client env: - VITE_SERVER_API : http://localhost:5050 - VITE_YOUTUBE_API : dummy - VITE_GITHUB_TOKEN : dummy - VITE_RAPIDAPI_KEY : dummy + VITE_SERVER_API: http://localhost:5050 + VITE_YOUTUBE_API: dummy + VITE_GITHUB_TOKEN: dummy + VITE_RAPIDAPI_KEY: dummy run: | - cd client - npm install - npm run dev || exit 1 & - sleep 15 - curl -f http://localhost:5173 || exit 1 - pkill -f "node" + npm run dev & + DEV_PID=$! + sleep 20 + if curl -f http://localhost:5173; then + echo "Frontend health check passed" + else + echo "Frontend health check failed" + exit 1 + fi + kill $DEV_PID Backend: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5.0.0 - with: - path: . + uses: actions/checkout@v4 - name: Setup Node - uses: actions/setup-node@v5.0.0 - - name: Install Dependencies + uses: actions/setup-node@v4 + with: + node-version: '18' + - name: Install Backend Dependencies + working-directory: ./server run: npm install - name: Check Server Directory - run: | - cd server - ls + working-directory: ./server + run: ls -la - name: Check Health + working-directory: ./server env: - MONGODB_URI : "mongodb+srv://publicuser:public_codify@cluster0.5bysaia.mongodb.net/" + MONGODB_URI: "mongodb+srv://publicuser:public_codify@cluster0.5bysaia.mongodb.net/" PORT: 5050 - JWT_SECRET : your_jwt_secret - CLIENT_CORS : "*" - EMAIL_USER : demo@gmail.com - EMAIL_PASS : demo1234 - GOOGLE_CLIENT_ID : dummy - GOOGLE_CLIENT_SECRET : dummy - GOOGLE_LOGIN_CALLBACK_URL : http://localhost:5050/api/v1/auth/google/login/callback - GOOGLE_SIGNUP_CALLBACK_URL : http://localhost:5050/api/v1/auth/google/signup/callback - FRONTEND_URL : http://localhost:5173 - YOUTUBE_API_KEY : dummy + JWT_SECRET: your_jwt_secret + CLIENT_CORS: "*" + EMAIL_USER: demo@gmail.com + EMAIL_PASS: demo1234 + GOOGLE_CLIENT_ID: dummy + GOOGLE_CLIENT_SECRET: dummy + GOOGLE_LOGIN_CALLBACK_URL: http://localhost:5050/api/v1/auth/google/login/callback + GOOGLE_SIGNUP_CALLBACK_URL: http://localhost:5050/api/v1/auth/google/signup/callback + FRONTEND_URL: http://localhost:5173 + YOUTUBE_API_KEY: dummy run: | - cd server - npm install - npm run server || exit 1 & - sleep 15 - curl -f http://localhost:5050 || exit 1 - pkill -f "node" + npm run server & + SERVER_PID=$! + sleep 20 + if curl -f http://localhost:5050; then + echo "Backend health check passed" + else + echo "Backend health check failed" + exit 1 + fi + kill $SERVER_PID diff --git a/.github/workflows/issue-create-automate-message.yml b/.github/workflows/issue-create-automate-message.yml index f03c6f64..14adc8d7 100644 --- a/.github/workflows/issue-create-automate-message.yml +++ b/.github/workflows/issue-create-automate-message.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Add Comment to Issue - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | const issueNumber = context.issue.number; diff --git a/.github/workflows/pr-create-automate-message.yml b/.github/workflows/pr-create-automate-message.yml index 1944de0c..5b88e57b 100644 --- a/.github/workflows/pr-create-automate-message.yml +++ b/.github/workflows/pr-create-automate-message.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Comment on PR - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | const prNumber = context.issue.number; diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml new file mode 100644 index 00000000..aa46df02 --- /dev/null +++ b/.github/workflows/test-build.yml @@ -0,0 +1,52 @@ +name: Test Build + +on: + workflow_dispatch: + +jobs: + test-frontend: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: client/package-lock.json + + - name: Install client dependencies + working-directory: ./client + run: npm ci + + - name: Build client + working-directory: ./client + env: + VITE_SERVER_API: http://localhost:5050 + VITE_YOUTUBE_API: dummy + VITE_GITHUB_TOKEN: dummy + VITE_RAPIDAPI_KEY: dummy + run: npm run build + + test-backend: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: server/package-lock.json + + - name: Install server dependencies + working-directory: ./server + run: npm ci + + - name: Check server syntax + working-directory: ./server + run: node --check server.js \ No newline at end of file diff --git a/client/src/App.jsx b/client/src/App.jsx index ad12f993..6de8ecda 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -54,7 +54,7 @@ const PythonFundamentals = lazy(() => import("./pages/Notes/PythonFundamentals/PythonFundamentals.jsx") ); const NodeJSFundamentals = lazy(() => - import("./pages/Notes/NodeJsFundamentals/NodeJsFundamentals.jsx") + import("./pages/Notes/NodeJSFundamentals/NodeJSFundamentals.jsx") ); // Admin layout diff --git a/client/src/pages/Signup.jsx b/client/src/pages/Signup.jsx index ac3026c3..7bc6959e 100644 --- a/client/src/pages/Signup.jsx +++ b/client/src/pages/Signup.jsx @@ -8,7 +8,7 @@ import { useLoading } from "../components/loadingContext"; import { FaUser, FaEnvelope, FaPhone, FaLock, FaUserPlus, FaExclamationCircle } from "react-icons/fa"; import { Link } from "react-router-dom"; import OtpModal from "../components/OtpModal"; -import PasswordStrength from "../../components/PasswordStrength"; +import PasswordStrength from "../components/PasswordStrength"; function Signup() { const [user, setUser] = useState({ diff --git a/package.json b/package.json index 8a4a7197..09e4ce73 100644 --- a/package.json +++ b/package.json @@ -5,23 +5,14 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "vite" + "install:client": "cd client && npm install", + "install:server": "cd server && npm install", + "install:all": "npm run install:client && npm run install:server", + "dev:client": "cd client && npm run dev", + "dev:server": "cd server && npm run server", + "build:client": "cd client && npm run build" }, "keywords": [], "author": "", - "license": "ISC", - "devDependencies": { - "vite": "^7.1.3" - }, - "dependencies": { - "@codemirror/lang-cpp": "^6.0.3", - "@codemirror/lang-javascript": "^6.2.4", - "@codemirror/lang-python": "^6.2.1", - "@codemirror/theme-one-dark": "^6.1.3", - "@uiw/react-codemirror": "^4.25.1", - "axios": "^1.11.0", - "quill": "^2.0.3", - "react": "^19.2.0", - "react-dom": "^19.2.0" - } + "license": "ISC" } diff --git a/server/config/passport.js b/server/config/passport.js index 2943375c..03d9bbb0 100644 --- a/server/config/passport.js +++ b/server/config/passport.js @@ -7,162 +7,174 @@ import dotenv from "dotenv"; dotenv.config(); export function configurePassport() { - // Google Login - passport.use( - "google-login", - new GoogleStrategy( - { - clientID: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - //callbackURL: "/api/v1/auth/google/login/callback", - callbackURL: process.env.GOOGLE_LOGIN_CALLBACK_URL, - prompt: "select_account", - }, - async (_, accessToken, refreshToken, profile, done) => { - try { - const email = profile.emails?.[0]?.value; - if (!email) return done(null, false, { message: "Email not found" }); - - let user = await User.findOne({ googleId: profile.id }); - if (!user) { - // Link existing account by email if exists (For users who signed up manually) - const existingUser = await User.findOne({ email }); - if (!existingUser) { - return done(null, false, { message: "Account not found. Please sign up first." }); + // Google Login - only if environment variables are set + if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET && process.env.GOOGLE_LOGIN_CALLBACK_URL) { + passport.use( + "google-login", + new GoogleStrategy( + { + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + //callbackURL: "/api/v1/auth/google/login/callback", + callbackURL: process.env.GOOGLE_LOGIN_CALLBACK_URL, + prompt: "select_account", + }, + async (_, accessToken, refreshToken, profile, done) => { + try { + const email = profile.emails?.[0]?.value; + if (!email) return done(null, false, { message: "Email not found" }); + + let user = await User.findOne({ googleId: profile.id }); + if (!user) { + // Link existing account by email if exists (For users who signed up manually) + const existingUser = await User.findOne({ email }); + if (!existingUser) { + return done(null, false, { message: "Account not found. Please sign up first." }); + } + existingUser.googleId = profile.id; + existingUser.isOAuth = true; + existingUser.avatar = profile.photos?.[0]?.value || ""; + await existingUser.save(); + user = existingUser; } - existingUser.googleId = profile.id; - existingUser.isOAuth = true; - existingUser.avatar = profile.photos?.[0]?.value || ""; - await existingUser.save(); - user = existingUser; - } - - const token = jwt.sign({ id: user._id, email: user.email }, process.env.JWT_SECRET, { - expiresIn: "30d", - }); - return done(null, { user, token }); - } catch (err) { - return done(err, null); - } - } - ) - ); - - // Google Signup - passport.use( - "google-signup", - new GoogleStrategy( - { - clientID: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - //callbackURL: "/api/v1/auth/google/signup/callback", - callbackURL: process.env.GOOGLE_SIGNUP_CALLBACK_URL, - prompt: "select_account", - }, - async (_, accessToken, refreshToken, profile, done) => { - try { - const email = profile.emails?.[0]?.value; - if (!email) return done(null, false, { message: "Email not found" }); - - let existingUser = await User.findOne({ email }); - if (existingUser) { - return done(null, false, { message: "Email already registered. Please login." }); - } + const token = jwt.sign({ id: user._id, email: user.email }, process.env.JWT_SECRET, { + expiresIn: "30d", + }); - // Create unique username - let username = profile.displayName?.replace(/\s+/g, "_") || `user_${Date.now()}`; - const usernameExists = await User.findOne({ username }); - if (usernameExists) { - username = `${username}_${profile.id.substring(0, 5)}`; + return done(null, { user, token }); + } catch (err) { + return done(err, null); } - - const user = await User.create({ - googleId: profile.id, - username, - email, - avatar: profile.photos?.[0]?.value || "", - isOAuth: true, - }); - - const token = jwt.sign({ id: user._id, email: user.email }, process.env.JWT_SECRET, { - expiresIn: "30d", - }); - - return done(null, { user, token }); - } catch (err) { - return done(err, null); } - } - ) - ); - - // GitHub Login + Signup (merged) - passport.use( - "github", - new GitHubStrategy( - { - clientID: process.env.GITHUB_CLIENT_ID, - clientSecret: process.env.GITHUB_CLIENT_SECRET, - callbackURL: process.env.GITHUB_LOGIN_CALLBACK_URL, - scope: ["user:email"], - }, - async (_, __, ___, profile, done) => { - try { - const emails = profile.emails || []; - const verifiedEmail = emails.find(e => e.verified && e.value)?.value; - const primaryEmail = emails.find(e => e.primary && e.value)?.value; - const anyEmail = emails.find(e => e.value)?.value; - - const email = verifiedEmail || primaryEmail || anyEmail; - - if (!email) { - return done(null, false, { - message: "No email found. Please make your GitHub email public or verify it.", - }); - } - - let user = await User.findOne({ email }); - let isNewUser = false; + ) + ); + } else { + console.log("Google Login OAuth not configured - skipping Google Login strategy registration"); + } + + // Google Signup - only if environment variables are set + if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET && process.env.GOOGLE_SIGNUP_CALLBACK_URL) { + passport.use( + "google-signup", + new GoogleStrategy( + { + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + //callbackURL: "/api/v1/auth/google/signup/callback", + callbackURL: process.env.GOOGLE_SIGNUP_CALLBACK_URL, + prompt: "select_account", + }, + async (_, accessToken, refreshToken, profile, done) => { + try { + const email = profile.emails?.[0]?.value; + if (!email) return done(null, false, { message: "Email not found" }); + + let existingUser = await User.findOne({ email }); + if (existingUser) { + return done(null, false, { message: "Email already registered. Please login." }); + } - if (!user) { - // Create new user if not found - isNewUser = true; - let username = profile.username || `user_${Date.now()}`; + // Create unique username + let username = profile.displayName?.replace(/\s+/g, "_") || `user_${Date.now()}`; const usernameExists = await User.findOne({ username }); if (usernameExists) { username = `${username}_${profile.id.substring(0, 5)}`; } - user = await User.create({ - githubId: profile.id, + const user = await User.create({ + googleId: profile.id, username, email, - avatar: profile.photos?.[0]?.value || `https://github.com/${profile.username}.png`, + avatar: profile.photos?.[0]?.value || "", isOAuth: true, }); - } else if (!user.githubId) { - // Link GitHub ID if existing user but no GitHub linked - user.githubId = profile.id; - user.isOAuth = true; - user.avatar = profile.photos?.[0]?.value || `https://github.com/${profile.username}.png`; - await user.save(); + + const token = jwt.sign({ id: user._id, email: user.email }, process.env.JWT_SECRET, { + expiresIn: "30d", + }); + + return done(null, { user, token }); + } catch (err) { + return done(err, null); } + } + ) + ); + } else { + console.log("Google Signup OAuth not configured - skipping Google Signup strategy registration"); + } + + // GitHub Login + Signup (merged) - only if environment variables are set + if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET && process.env.GITHUB_LOGIN_CALLBACK_URL) { + passport.use( + "github", + new GitHubStrategy( + { + clientID: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackURL: process.env.GITHUB_LOGIN_CALLBACK_URL, + scope: ["user:email"], + }, + async (_, __, ___, profile, done) => { + try { + const emails = profile.emails || []; + const verifiedEmail = emails.find(e => e.verified && e.value)?.value; + const primaryEmail = emails.find(e => e.primary && e.value)?.value; + const anyEmail = emails.find(e => e.value)?.value; + + const email = verifiedEmail || primaryEmail || anyEmail; + + if (!email) { + return done(null, false, { + message: "No email found. Please make your GitHub email public or verify it.", + }); + } + + let user = await User.findOne({ email }); + let isNewUser = false; + + if (!user) { + // Create new user if not found + isNewUser = true; + let username = profile.username || `user_${Date.now()}`; + const usernameExists = await User.findOne({ username }); + if (usernameExists) { + username = `${username}_${profile.id.substring(0, 5)}`; + } + + user = await User.create({ + githubId: profile.id, + username, + email, + avatar: profile.photos?.[0]?.value || `https://github.com/${profile.username}.png`, + isOAuth: true, + }); + } else if (!user.githubId) { + // Link GitHub ID if existing user but no GitHub linked + user.githubId = profile.id; + user.isOAuth = true; + user.avatar = profile.photos?.[0]?.value || `https://github.com/${profile.username}.png`; + await user.save(); + } - const token = jwt.sign( - { id: user._id, email: user.email }, - process.env.JWT_SECRET, - { expiresIn: "30d" } - ); + const token = jwt.sign( + { id: user._id, email: user.email }, + process.env.JWT_SECRET, + { expiresIn: "30d" } + ); - return done(null, { user, token, isNewUser }); - } catch (err) { - console.error("Error in GitHub strategy:", err); - return done(err, null); + return done(null, { user, token, isNewUser }); + } catch (err) { + console.error("Error in GitHub strategy:", err); + return done(err, null); + } } - } - ) - ); + ) + ); + } else { + console.log("GitHub OAuth not configured - skipping GitHub strategy registration"); + } passport.serializeUser((data, done) => done(null, data)); passport.deserializeUser((data, done) => done(null, data)); diff --git a/server/package-lock.json b/server/package-lock.json index 10ecb7d6..d29affff 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -76,7 +76,6 @@ "node_modules/@firebase/app": { "version": "0.10.15", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/component": "0.6.10", "@firebase/logger": "0.4.3", @@ -133,7 +132,6 @@ "node_modules/@firebase/app-compat": { "version": "0.2.45", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/app": "0.10.15", "@firebase/component": "0.6.10", @@ -147,8 +145,7 @@ }, "node_modules/@firebase/app-types": { "version": "0.9.2", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@firebase/auth": { "version": "1.8.0", @@ -533,7 +530,6 @@ "node_modules/@firebase/util": { "version": "1.10.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -1273,7 +1269,6 @@ "node_modules/firebase": { "version": "11.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/analytics": "0.10.9", "@firebase/analytics-compat": "0.2.15",