diff --git a/backend/.env.sample b/backend/.env.sample index dbbc24a..a6d4cf1 100644 --- a/backend/.env.sample +++ b/backend/.env.sample @@ -4,4 +4,9 @@ GITHUB_CLIENT_SECRET= GITHUB_CALLBACK_URL= JWT_SECRET_KEY= JWT_REFRESH_SECRET_KEY= -REDIS_URL= \ No newline at end of file +REDIS_PASSWORD= +REDIS_HOST= +REDIS_PORT= +GITHUB_WEBHOOK_SECRET= +EMAIL_USER= +EMAIL_PASS= diff --git a/backend/config/redisClient.js b/backend/config/redisClient.js index 62ca171..118fd57 100644 --- a/backend/config/redisClient.js +++ b/backend/config/redisClient.js @@ -1,11 +1,15 @@ import { createClient } from 'redis'; const redisClient = createClient({ - url: process.env.REDIS_URL || 'redis://localhost:6379' + username: 'default', + password: process.env.REDIS_PASSWORD, + socket: { + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT + } }); redisClient.on('error', (err) => console.error('Redis Client Error', err)); -await redisClient.connect(); export default redisClient; diff --git a/backend/config/redisConnect.js b/backend/config/redisConnect.js new file mode 100644 index 0000000..0bfc920 --- /dev/null +++ b/backend/config/redisConnect.js @@ -0,0 +1,9 @@ +import redisClient from "./redisClient.js"; + +async function redisConnect() { + redisClient.on('error', err => console.log('Redis Client Error', err)); + console.log('Redis client connected'); + await redisClient.connect(); +} + +export default redisConnect ; \ No newline at end of file diff --git a/backend/controllers/auth/callback.js b/backend/controllers/auth/callback.js new file mode 100644 index 0000000..31ee81f --- /dev/null +++ b/backend/controllers/auth/callback.js @@ -0,0 +1,4 @@ +export default function callback(req, res) { + const code = req.query.code; + res.redirect(`http://localhost:5173/login?code=${code}`); +} \ No newline at end of file diff --git a/backend/controllers/auth/exchangecode.js b/backend/controllers/auth/exchangecode.js index be0ff62..e8f8406 100644 --- a/backend/controllers/auth/exchangecode.js +++ b/backend/controllers/auth/exchangecode.js @@ -1,7 +1,6 @@ - import jwt from "jsonwebtoken"; import axios from "axios"; -import UserModel from "../../models/user/userSchema.js"; +import UserModel from "../../models/user/userSchema.js"; export default async function exchangeToken(req, res) { const code = req.params.code; @@ -23,7 +22,21 @@ export default async function exchangeToken(req, res) { { headers: { Accept: "application/json" } } ); - const access_token = tokenResponse.data.access_token; + const { + access_token, + refresh_token, + refresh_token_expires_in, + expires_in, + } = tokenResponse.data; + + const ghAccessTokenExpiresAt = expires_in + ? new Date(Date.now() + expires_in * 1000) + : new Date(Date.now() + 2 * 60 * 60 * 1000); + + const ghRefreshTokenExpiresAt = refresh_token_expires_in + ? new Date(Date.now() + refresh_token_expires_in * 1000) + : new Date(Date.now() + 180 * 24 * 60 * 60 * 1000); + if (!access_token) { return res.status(400).json({ success: false, @@ -37,52 +50,46 @@ export default async function exchangeToken(req, res) { const ghUser = userResponse.data; - - // Encrypting the GitHub token using jwt before sorting - const encryptedAccessToken = jwt.sign( - { access_token }, - process.env.JWT_SECRET_KEY, - { expiresIn: "7d" } - ); - let user = await UserModel.findOne({ userId: ghUser.id }); if (!user) { user = new UserModel({ userId: ghUser.id, email: ghUser.email || `${ghUser.login}@users.noreply.github.com`, username: ghUser.login, - accessToken: encryptedAccessToken, + avatarUrl: ghUser.avatar_url, + accessToken: access_token, + accessTokenExpiresAt: ghAccessTokenExpiresAt, + refreshToken: refresh_token, + refreshTokenExpiresAt: ghRefreshTokenExpiresAt, }); } else { - user.accessToken = encryptedAccessToken; + user.accessToken = access_token; + user.accessTokenExpiresAt = ghAccessTokenExpiresAt; + user.refreshToken = refresh_token; + user.refreshTokenExpiresAt = ghRefreshTokenExpiresAt; + user.avatarUrl = ghUser.avatar_url; } - // Generate access and refresh tokens const accessToken = jwt.sign( { id: user.userId, username: user.username, email: user.email, + avatarUrl: user.avatarUrl, + ghTokenExpiresAt: ghAccessTokenExpiresAt, + ghAccessToken: access_token, }, process.env.JWT_SECRET_KEY, - { expiresIn: "2hr" } // access token vaise 1hr ya usse kam rkhna chiye + { expiresIn: "4d" } ); - const refreshToken = jwt.sign( - { - id: user.userId, - username: user.username, - email: user.email, - }, - process.env.JWT_REFRESH_SECRET_KEY, - { expiresIn: "7d" } // refresh token abhi k liye 7 din bad expire hoga - ); - - user.refreshToken = refreshToken; await user.save(); - res.cookie("accesstoken", accessToken, { httpOnly: true, maxAge: 2 * 60 * 60 * 1000 }); - res.cookie("refreshToken", refreshToken, { httpOnly: true, maxAge: 7 * 24 * 60 * 60 * 1000 }); + res.cookie("accesstoken", accessToken, { + httpOnly: true, + maxAge: 2 * 60 * 60 * 1000, + }); + req.user = user ; res.status(201).json({ success: true, message: "User logged in successfully", @@ -90,6 +97,7 @@ export default async function exchangeToken(req, res) { username: user.username, email: user.email, userId: user.userId, + avatarUrl: user.avatarUrl, }, }); } catch (err) { diff --git a/backend/controllers/auth/getRepoPullRequests.js b/backend/controllers/auth/getRepoPullRequests.js new file mode 100644 index 0000000..fd58d7c --- /dev/null +++ b/backend/controllers/auth/getRepoPullRequests.js @@ -0,0 +1,35 @@ +import getDecryptedGithubToken from "../../utils/decryptGithubToken.js"; +import axios from "axios"; + +export default async function getRepoPullRequests(req, res) { + try { + // Use authenticated user + const user = req.user; + if (!user || !user.userId) return res.status(401).json({ success: false, message: "User not authenticated" }); + const accesstoken = req.cookies.accesstoken; + if (!accesstoken) return res.status(401).json({ success: false, message: "GitHub token not found" }); + const payload = await getDecryptedGithubToken(accesstoken); + // console.log(payload); + if (!payload) return res.status(401).json({ success: false, message: "GitHub data not found" }); + + // Repo name comes from the URL; owner is the authenticated user's GitHub username + const repo = req.params.repo; + // console.log(repo); + + const owner = user.username; + if (!repo) return res.status(400).json({ success: false, message: "Missing repo name" }); + //console.log(payload.ghAccessToken); + + const per_page = (req.pagination && req.pagination.limit) ? req.pagination.limit : 5; + const page = (req.pagination && req.pagination.page) ? req.pagination.page : 1; + + const response = await axios.get(`https://api.github.com/repos/${owner}/${repo}/pulls`, { + headers: { Authorization: `token ${payload.ghAccessToken}` }, + params: { state: "open", per_page, page } + }); + res.json({ success: true, pulls: response.data, pagination: req.pagination || { limit: per_page, page } }); + } catch (err) { + console.error("getRepoPullRequests error:", err.message); + res.status(500).json({ success: false, message: err.message }); + } +} \ No newline at end of file diff --git a/backend/controllers/auth/getUserRepos.js b/backend/controllers/auth/getUserRepos.js new file mode 100644 index 0000000..c8c7bf4 --- /dev/null +++ b/backend/controllers/auth/getUserRepos.js @@ -0,0 +1,59 @@ +import getDecryptedGithubToken from "../../utils/decryptGithubToken.js"; +import axios from "axios"; + +export default async function getUserRepos(req, res) { + try { + const user = req.user; + if (!user || !user.userId) { + return res.status(401).json({ success: false, message: "User not authenticated" }); + } + + const accessToken = req.cookies.accesstoken; + if (!accessToken) { + return res.status(401).json({ success: false, message: "GitHub token not found" }); + } + + const payload = await getDecryptedGithubToken(accessToken); + + // Use pagination middleware values if present, otherwise fall back + const per_page = (req.pagination && req.pagination.limit) ? req.pagination.limit : 5; + const page = (req.pagination && req.pagination.page) ? req.pagination.page : 1; + + const response = await axios.get("https://api.github.com/user/repos", { + headers: { Authorization: `token ${payload.ghAccessToken}` }, + params: { page, per_page, sort: "updated" } + }); + + const repos = response.data || []; + + const filtered = repos.map(repo => ({ + id: repo.id, + name: repo.name, + full_name: repo.full_name, + private: repo.private, + url: repo.url, + + owner: repo.owner ? { + login: repo.owner.login, + id: repo.owner.id, + avatar_url: repo.owner.avatar_url, + html_url: repo.owner.html_url, + type: repo.owner.type + } : null, + + html_url: repo.html_url, + description: repo.description, + forks_count: repo.forks_count, + stargazers_count: repo.stargazers_count, + watchers_count: repo.watchers_count, + open_issues_count: repo.open_issues_count, + language: repo.language + })); + + return res.json({ success: true, repos: filtered, pagination: req.pagination || { limit: per_page, page } }); + + } catch (err) { + console.error("getUserRepos error:", err.message); + return res.status(500).json({ success: false, message: err.message }); + } +} diff --git a/backend/controllers/auth/logoutuser.js b/backend/controllers/auth/logoutuser.js index d0910d4..ab912a2 100644 --- a/backend/controllers/auth/logoutuser.js +++ b/backend/controllers/auth/logoutuser.js @@ -19,7 +19,18 @@ export default async function logoutUser(req, res) { } } - const userId = req.user?.id || req.body.userId; + + // Extract userId from access token payload (verify signature) + let userId; + if (accessToken) { + try { + const payload = jwt.verify(accessToken, process.env.JWT_SECRET_KEY); + userId = payload.id; + console.log("userid found: " + userId); + } catch (e) { + throw new error ("userid couldnt be found" + e); + } + } if (userId) { await UserModel.updateOne({ userId }, { $unset: { refreshToken: "" } }); } diff --git a/backend/controllers/auth/redirect.js b/backend/controllers/auth/redirect.js index dbf9591..57259ae 100644 --- a/backend/controllers/auth/redirect.js +++ b/backend/controllers/auth/redirect.js @@ -27,7 +27,7 @@ * // User visits endpoint triggering this function * // Gets redirected to: https://github.com/login/oauth/authorize?client_id=123&redirect_uri=https://app.com/callback&scope=read:user%20user:email%20repo */ -export default function redirectUser(req,res){ - const redirectURL = `https://github.com/login/oauth/authorize?client_id=${process.env.GITHUB_CLIENT_ID}&redirect_uri=${process.env.GITHUB_CALLBACK_URL}&scope=read:user%20user:email%20repo` - res.redirect(redirectURL) ; +export default function redirectUser(req, res) { + const redirectURL = `https://github.com/login/oauth/authorize?client_id=${process.env.GITHUB_CLIENT_ID}&redirect_uri=http://localhost:3000/auth/callback&scope=read:user%20user:email%20repo` + res.redirect(redirectURL); } \ No newline at end of file diff --git a/backend/controllers/auth/refreshToken.js b/backend/controllers/auth/refreshToken.js deleted file mode 100644 index a56a626..0000000 --- a/backend/controllers/auth/refreshToken.js +++ /dev/null @@ -1,31 +0,0 @@ -import jwt from "jsonwebtoken"; -import UserModel from "../../models/user/userSchema.js"; - -export default async function refreshTokenController(req, res) { - const refreshToken = req.cookies.refreshToken || req.body.refreshToken; - if (!refreshToken) { - return res.status(401).json({ success: false, message: "No refresh token provided" }); - } - try { - // Verify refresh token - const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET_KEY); - const user = await UserModel.findOne({ userId: decoded.id, refreshToken }); - if (!user) { - return res.status(403).json({ success: false, message: "Invalid refresh token" }); - } - // Issueing new access token - const newAccessToken = jwt.sign( - { - id: user.userId, - username: user.username, - email: user.email, - }, - process.env.JWT_SECRET_KEY, - { expiresIn: "15m" } - ); - res.cookie("accesstoken", newAccessToken, { httpOnly: true, maxAge: 2 * 60 * 60 * 1000 }); - res.status(200).json({ success: true, accessToken: newAccessToken }); - } catch (err) { - return res.status(403).json({ success: false, message: "Invalid or expired refresh token" }); - } -} \ No newline at end of file diff --git a/backend/controllers/llm/analysePr.js b/backend/controllers/llm/analysePr.js new file mode 100644 index 0000000..c0d953a --- /dev/null +++ b/backend/controllers/llm/analysePr.js @@ -0,0 +1,85 @@ +import getDecryptedGithubToken from "../../utils/decryptGithubToken.js"; +import axios from "axios"; +import { encode } from "@toon-format/toon"; +import cleanDiff from "../../utils/cleanDIff.js"; +import compressForLLM from "../../utils/compressllm.js"; +import prioritizeFiles from "../../utils/smartfile.js"; + +export default async function analysePr(req, res) { + try { + const accesstoken = req.cookies.accesstoken; + if (!accesstoken) { + return res.status(401).json({ + success: false, + message: "github token not found", + }); + } + + const payload = await getDecryptedGithubToken(accesstoken); + if (!payload) { + return res.status(401).json({ + success: false, + message: "github data not found", + }); + } + + const { owner, repo, prNumber } = req.params; + const headers = { + Authorization: `token ${payload.ghAccessToken}`, + "User-Agent": "pullshark",// ye bheja h kyuki github api me user-agent dena zaruri hota h + }; + + const baseUrl = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`; + + const [metaRes, diffRes, filesRes] = await Promise.all([ + axios.get(baseUrl, { headers }), + axios.get(baseUrl, { + headers: { ...headers, Accept: "application/vnd.github.v3.diff" }, + }), + axios.get(`${baseUrl}/files`, { headers }), + ]); + + // Apply all optimizations + const cleanedDiff = cleanDiff(diffRes.data); + const prioritizedFiles = prioritizeFiles(filesRes.data); + + const minimalResponse = compressForLLM({ + title: metaRes.data.title, + description: metaRes.data.body, + author: metaRes.data.user?.login, + changedFiles: prioritizedFiles.map((f) => f.filename), + diff: cleanedDiff, + }); + + const encoded = encode(minimalResponse); + const sizeInBytes = Buffer.byteLength(encoded, "utf8"); + const sizeInKB = (sizeInBytes / 1024).toFixed(2); + const base64Payload = Buffer.from(encoded, "utf8").toString("base64"); + // console.log(base64Payload); + const response = await fetch( + "https://pullshark-ai.onrender.com/api/analyze", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ pr: base64Payload }), + } + ); + + const data = await response.json(); + + // console.log(data); + return res.status(200).json({ + success: true, + sizeInKB, + sizeInBytes, + data, + }); + } catch (err) { + return res.status(500).json({ + success: false, + message: err.message, + }); + } +} diff --git a/backend/controllers/webhook/handleWebhook.js b/backend/controllers/webhook/handleWebhook.js new file mode 100644 index 0000000..965e466 --- /dev/null +++ b/backend/controllers/webhook/handleWebhook.js @@ -0,0 +1,163 @@ +import crypto from "crypto"; +import axios from "axios"; +import { encode } from "@toon-format/toon"; +import sendEmail from "../../utils/sendEmail.js"; // Import the email utility +import cleanDiff from "../../utils/cleanDIff.js"; +import compressForLLM from "../../utils/compressllm.js"; +import prioritizeFiles from "../../utils/smartfile.js"; +import { log } from "console"; // Keep this if you use log() elsewhere + +// Controller: handle incoming GitHub webhooks +export default async function handleWebhook(req, res) { + try { + const secret = process.env.GITHUB_WEBHOOK_SECRET; + if (!secret) { + return res.status(500).json({ success: false, message: "GITHUB_WEBHOOK_SECRET not configured" }); + } + + const signatureHeader = + req.headers["x-hub-signature-256"] || req.headers["x-hub-signature"]; + if (!signatureHeader) { + return res.status(400).json({ success: false, message: "Missing signature header" }); + } + + // ------------------------- + // FIXED RAW BODY HANDLING + // ------------------------- + const rawBody = + req.rawBody && Buffer.isBuffer(req.rawBody) + ? req.rawBody + : req.body && Buffer.isBuffer(req.body) + ? req.body + : Buffer.from(JSON.stringify(req.body || {})); + + if (!rawBody || !rawBody.length) { + return res.status(400).json({ success: false, message: "Missing raw body" }); + } + + // Signature compute + const computedHash = crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); + const expectedSignature = `sha256=${computedHash}`; + + const sigBuf = Buffer.from(signatureHeader); + const expBuf = Buffer.from(expectedSignature); + + if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) { + return res.status(401).json({ success: false, message: "Invalid signature" }); + } + + // Parse payload + let payload; + try { + payload = JSON.parse(rawBody.toString()); + console.log("Webhook payload received"); + } catch { + return res.status(400).json({ success: false, message: "Invalid JSON payload" }); + } + + const event = req.headers["x-github-event"] || req.headers["x-hub-event"]; + if (event !== "pull_request") { + return res.status(200).json({ success: true, message: `Ignored event: ${event}` }); + } + + const action = payload.action; + const pr = payload.pull_request; + const repo = payload.repository; + const owner = repo?.owner?.login || pr?.head?.repo?.owner?.login; + const repoName = repo?.name || pr?.head?.repo?.name; + const prNumber = pr?.number; + + const actionable = ["opened", "reopened", "synchronize", "edited"]; + if (!actionable.includes(action)) { + return res.status(200).json({ success: true, message: `Action ${action} ignored` }); + } + + const headers = { "User-Agent": "pullshark" }; + + // --- EMAIL LOGIC: Fetch user email from commit --- + let userEmail = null; + try { + const commitsUrl = pr.commits_url; + const commits = (await axios.get(commitsUrl, { headers })).data; + if (commits && commits.length > 0) { + // Get email from the author of the latest commit + userEmail = commits[commits.length - 1].commit.author.email; + console.log(`Found user email from commit: ${userEmail}`); + } + } catch (err) { + console.warn("Could not fetch commit details to get user email:", err.message); + } + + // --- EMAIL LOGIC: Send "Analysis Started" email --- + if (userEmail) { + sendEmail({ + to: userEmail, + subject: `[PullShark] Analysis started for PR #${prNumber}`, + text: `Hi ${pr.user?.login},\n\nWe've started analyzing your pull request: "${pr.title}".\n\nYou'll receive another email once the analysis is complete.\n\n- The PullShark Team`, + html: `
Hi ${pr.user?.login},
We've started analyzing your pull request: "${pr.title}".
You'll receive another email once the analysis is complete.
- The PullShark Team
`, + }).catch(err => console.error("Failed to send 'analysis started' email:", err.message)); + } + + // Fetch diff + const diffUrl = pr.diff_url; + let diffText = ""; + try { + diffText = (await axios.get(diffUrl, { headers, responseType: "text" })).data; + } catch (err) { + console.warn("Failed to fetch diff:", err.message); + } + + // Fetch changed files + let filesList = []; + try { + const filesUrl = pr.url + "/files"; + filesList = (await axios.get(filesUrl, { headers })).data; + } catch (err) { + console.warn("Failed to fetch PR files:", err.message); + } + + const cleanedDiff = cleanDiff(diffText); + const prioritized = prioritizeFiles(filesList); + + const minimalResponse = compressForLLM({ + title: pr.title, + description: pr.body, + author: pr.user?.login, + changedFiles: prioritized.map(f => f.filename), + diff: cleanedDiff, + }); + + const encoded = encode(minimalResponse); + const base64Payload = Buffer.from(encoded, "utf8").toString("base64"); + + try { + const response = await axios.post( + "https://pullshark-ai.onrender.com/api/analyze", + { pr: base64Payload }, + { headers: { "Content-Type": "application/json" } } + ); + + // --- EMAIL LOGIC: Send "Analysis Complete" email --- + if (userEmail) { + // Assuming response.data contains the analysis text or object + const analysisResult = JSON.stringify(response.data, null, 2); + + sendEmail({ + to: userEmail, + subject: `[PullShark] Analysis complete for PR #${prNumber}`, + text: `Hi ${pr.user?.login},\n\nYour pull request analysis is complete.\n\nResult:\n${analysisResult}\n\n- The PullShark Team`, + html: `Hi ${pr.user?.login},
Your pull request analysis is complete.
${analysisResult}- The PullShark Team
`, + }).catch(err => console.error("Failed to send 'analysis complete' email:", err.message)); + } + // --- END EMAIL LOGIC --- + + return res.status(200).json({ success: true, model: response.data }); + } catch (err) { + console.error("Model analyze call failed:", err.message); + return res.status(500).json({ success: false, message: "Model analyze failed" }); + } + } catch (err) { + console.error("Webhook handler error:", err.message); + return res.status(500).json({ success: false, message: err.message }); + } +} diff --git a/backend/middlewares/pagination.js b/backend/middlewares/pagination.js new file mode 100644 index 0000000..2bcedf5 --- /dev/null +++ b/backend/middlewares/pagination.js @@ -0,0 +1,27 @@ +const DEFAULT_LIMIT = 5; +const MAX_LIMIT = 100; +const DEFAULT_PAGE = 1; + +export default function paginationMiddleware(req, res, next) { + try { + const { limit: qLimit, page: qPage } = req.query || {}; + + let limit = parseInt(qLimit, 10); + let page = parseInt(qPage, 10); + + if (Number.isNaN(limit) || limit <= 0) limit = DEFAULT_LIMIT; + if (limit > MAX_LIMIT) limit = MAX_LIMIT; + + if (Number.isNaN(page) || page <= 0) page = DEFAULT_PAGE; + + req.pagination = { + limit, + page, + skip: (page - 1) * limit + }; + + next(); + } catch (err) { + next(err); + } +} diff --git a/backend/middlewares/ratelimiter.js b/backend/middlewares/ratelimiter.js new file mode 100644 index 0000000..1ac46bc --- /dev/null +++ b/backend/middlewares/ratelimiter.js @@ -0,0 +1,34 @@ + +import redisClient from "../config/redisClient.js"; + +// Fixed window size and max requests +const windowSize = 60; // seconds +const maxRequests = 60; // max requests per window + +export default async function rateLimiter(req, res, next) { + try { + const key = `rl_${req.ip}`; + const now = Date.now(); + const windowStart = now - windowSize * 1000; + + // Remove old timestamps + await redisClient.zRemRangeByScore(key, 0, windowStart); + + // Get current count + const reqCount = await redisClient.zCard(key); + + if (reqCount >= maxRequests) { + return res.status(429).json({ success: false, message: "Too many requests. Please try again later." }); + } + console.log("Request count: " + reqCount); + // Add current timestamp + await redisClient.zAdd(key, [{ score: now, value: `${now}` }]); + // Set expiry for the key + await redisClient.expire(key, windowSize); + + next(); + } catch (err) { + return res.status(500).json({ success: false, message: "Rate limiter error" }); + } +} + diff --git a/backend/middlewares/refreshTokenMiddleware.js b/backend/middlewares/refreshTokenMiddleware.js new file mode 100644 index 0000000..53604c4 --- /dev/null +++ b/backend/middlewares/refreshTokenMiddleware.js @@ -0,0 +1,139 @@ +import jwt from "jsonwebtoken"; +import axios from "axios"; +import UserModel from "../models/user/userSchema.js"; + +const REFRESH_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes in milliseconds + +export default async function refreshGitHubTokenMiddleware(req, res, next) { + const accessToken = req.cookies.accesstoken; + + if (!accessToken) { + return res + .status(401) + .json({ success: false, message: "No access token provided" }); + } + + let decodedAppToken; + try { + // 1. Verify the application's JWT + decodedAppToken = jwt.verify(accessToken, process.env.JWT_SECRET_KEY); + } catch (err) { + // This catches errors if the *app's* JWT is expired or invalid + if (err.name === "TokenExpiredError") { + return res + .status(401) + .json({ success: false, message: "App session expired. Please log in again." }); + } + return res + .status(401) + .json({ success: false, message: "Invalid app token." }); + } + + try { + // 2. Fetch the user from the database + const user = await UserModel.findOne({ userId: decodedAppToken.id }); + if (!user) { + return res.status(403).json({ success: false, message: "User not found" }); + } + + // 3. Check the GitHub access token's expiry (from the DB) + const ttl_ms = new Date(user.accessTokenExpiresAt).getTime() - Date.now(); + + // 4. Check if the token needs refreshing + if (ttl_ms <= REFRESH_THRESHOLD_MS) { + // GitHub token is expiring soon or has expired, time to refresh + + // Check if GitHub refresh token itself is expired + if ( + user.refreshTokenExpiresAt && + new Date() > user.refreshTokenExpiresAt + ) { + return res + .status(403) + .json({ + success: false, + message: "GitHub refresh token expired. Please log in again.", + }); + } + + // Request new GitHub access token + const tokenResponse = await axios.post( + "https://github.com/login/oauth/access_token", + { + client_id: process.env.GITHUB_CLIENT_ID, + client_secret: process.env.GITHUB_CLIENT_SECRET, + grant_type: "refresh_token", + refresh_token: user.refreshToken, + }, + { headers: { Accept: "application/json" } } + ); + + const { + access_token: newAccessToken, + refresh_token: newRefreshToken, + expires_in: newExpiresIn, + refresh_token_expires_in: newRefreshTokenExpiresIn, + } = tokenResponse.data; + + if (!newAccessToken) { + return res + .status(400) + .json({ success: false, message: "Failed to refresh GitHub token" }); + } + + // Update user document with new token info + user.accessToken = newAccessToken; + user.accessTokenExpiresAt = new Date(Date.now() + newExpiresIn * 1000); + if (newRefreshToken) { + user.refreshToken = newRefreshToken; + } + if (newRefreshTokenExpiresIn) { + user.refreshTokenExpiresAt = new Date( + Date.now() + newRefreshTokenExpiresIn * 1000 + ); + } + + await user.save(); + + // We must also re-issue our *app's* JWT because it contains + // the (now stale) GitHub token expiry. + const newAppAccessToken = jwt.sign( + { + id: user.userId, + username: user.username, + email: user.email, + ghTokenExpiresAt: user.accessTokenExpiresAt, // Store the new expiry + }, + process.env.JWT_SECRET_KEY, + { expiresIn: "4d" } // Or your app's standard expiry + ); + + // Set the new cookie + res.cookie("accesstoken", newAppAccessToken, { + httpOnly: true, + maxAge: 2 * 60 * 60 * 1000, + }); + + // Attach the updated user to the request for the next handler + req.user = user; + + } else { + // Token is fine, just attach user to request + req.user = user; + } + + // 5. All good, proceed to the actual route handler + next(); + + } catch (err) { + // Catches errors from DB query or GitHub API call + console.error("GitHub Token Refresh Middleware Error:", err.message); + // Check if it's a GitHub API error (e.g., bad refresh token) + if (err.response && err.response.data) { + return res.status(403).json({ success: false, message: "GitHub API error.", error: err.response.data }); + } + return res + .status(500) + .json({ success: false, message: "Internal server error during token refresh." }); + } +} \ No newline at end of file diff --git a/backend/models/user/userSchema.js b/backend/models/user/userSchema.js index 3fd26c4..2b0f36f 100644 --- a/backend/models/user/userSchema.js +++ b/backend/models/user/userSchema.js @@ -10,8 +10,9 @@ const userSchema = new mongoose.Schema({ userId: { type: String, required: true }, username: { type: String, required: true }, email: { type: String, required: true }, - accessToken: { type: String, required: true }, - refreshToken: { type: String },// will be set when user logs in + avatarUrl: { type: String, default: ""}, + refreshToken: { type: String }, + refreshTokenExpiresAt: { type: Date }, connectedRepos: [connectedRepoSchema], usageStats: { testRuns: { type: Number, default: 0 } }, }, { timestamps: true }); diff --git a/backend/package-lock.json b/backend/package-lock.json index e180f62..dcec3b8 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -5,12 +5,17 @@ "packages": { "": { "dependencies": { + "@toon-format/toon": "^2.0.0", "axios": "^1.13.2", "cookie-parser": "^1.4.7", + "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", - "mongoose": "^8.19.3" + "mongoose": "^8.19.3", + "ngrok": "^5.0.0-beta.2", + "nodemailer": "^7.0.11", + "redis": "^5.9.0" } }, "node_modules/@mongodb-js/saslprep": { @@ -22,6 +27,141 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@redis/bloom": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.9.0.tgz", + "integrity": "sha512-W9D8yfKTWl4tP8lkC3MRYkMz4OfbuzE/W8iObe0jFgoRmgMfkBV+Vj38gvIqZPImtY0WB34YZkX3amYuQebvRQ==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.9.0" + } + }, + "node_modules/@redis/client": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.9.0.tgz", + "integrity": "sha512-EI0Ti5pojD2p7TmcS7RRa+AJVahdQvP/urpcSbK/K9Rlk6+dwMJTQ354pCNGCwfke8x4yKr5+iH85wcERSkwLQ==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@redis/json": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.9.0.tgz", + "integrity": "sha512-Bm2jjLYaXdUWPb9RaEywxnjmzw7dWKDZI4MS79mTWPV16R982jVWBj6lY2ZGelJbwxHtEVg4/FSVgYDkuO/MxA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.9.0" + } + }, + "node_modules/@redis/search": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.9.0.tgz", + "integrity": "sha512-jdk2csmJ29DlpvCIb2ySjix2co14/0iwIT3C0I+7ZaToXgPbgBMB+zfEilSuncI2F9JcVxHki0YtLA0xX3VdpA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.9.0" + } + }, + "node_modules/@redis/time-series": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.9.0.tgz", + "integrity": "sha512-W6ILxcyOqhnI7ELKjJXOktIg3w4+aBHugDbVpgVLPZ+YDjObis1M0v7ZzwlpXhlpwsfePfipeSK+KWNuymk52w==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.9.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@toon-format/toon": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@toon-format/toon/-/toon-2.0.0.tgz", + "integrity": "sha512-7lIVGzCZ9MzKKcbWOumUO4CXB1owpAwDhvDzpjmyqT0B8l9fROMyEMeSubJrGc8y0IxpGZgwt/8PpCOVLiB7UA==", + "license": "MIT" + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -37,6 +177,16 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -68,23 +218,27 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/bson": { @@ -96,6 +250,15 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -111,6 +274,33 @@ "node": ">= 0.8" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -140,6 +330,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -210,6 +421,19 @@ "node": ">=6.6.0" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -227,6 +451,42 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -295,6 +555,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -397,6 +666,35 @@ "url": "https://opencollective.com/express" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", @@ -535,6 +833,21 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -547,6 +860,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -586,6 +924,19 @@ "node": ">= 0.4" } }, + "node_modules/hpagent": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-0.1.2.tgz", + "integrity": "sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==", + "license": "MIT", + "optional": true + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -611,16 +962,33 @@ "node": ">= 0.8" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/inherits": { @@ -644,6 +1012,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -696,6 +1070,21 @@ "node": ">=12.0.0" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -738,6 +1127,15 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -795,6 +1193,15 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/mongodb": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", @@ -909,6 +1316,59 @@ "node": ">= 0.6" } }, + "node_modules/ngrok": { + "version": "5.0.0-beta.2", + "resolved": "https://registry.npmjs.org/ngrok/-/ngrok-5.0.0-beta.2.tgz", + "integrity": "sha512-UzsyGiJ4yTTQLCQD11k1DQaMwq2/SsztBg2b34zAqcyjS25qjDpogMKPaCKHwe/APRTHeel3iDXcVctk5CNaCQ==", + "hasInstallScript": true, + "license": "BSD-2-Clause", + "dependencies": { + "extract-zip": "^2.0.1", + "got": "^11.8.5", + "lodash.clonedeep": "^4.5.0", + "uuid": "^7.0.0 || ^8.0.0", + "yaml": "^2.2.2" + }, + "bin": { + "ngrok": "bin/ngrok" + }, + "engines": { + "node": ">=14.2" + }, + "optionalDependencies": { + "hpagent": "^0.1.2" + } + }, + "node_modules/nodemailer": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -942,6 +1402,15 @@ "wrappy": "1" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -961,6 +1430,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -980,6 +1455,16 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -1004,6 +1489,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1028,20 +1525,38 @@ "node": ">= 0.10" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "node_modules/redis": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-5.9.0.tgz", + "integrity": "sha512-E8dQVLSyH6UE/C9darFuwq4usOPrqfZ1864kI4RFbr5Oj9ioB9qPF0oJMwX7s8mf6sPYrz84x/Dx1PGF3/0EaQ==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "@redis/bloom": "5.9.0", + "@redis/client": "5.9.0", + "@redis/json": "5.9.0", + "@redis/search": "5.9.0", + "@redis/time-series": "5.9.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 18" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/router": { @@ -1272,6 +1787,12 @@ "node": ">= 0.6" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1281,6 +1802,15 @@ "node": ">= 0.8" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1317,6 +1847,28 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } } } } diff --git a/backend/package.json b/backend/package.json index 09bb8aa..3bffeba 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,11 +1,16 @@ { "type": "module", "dependencies": { + "@toon-format/toon": "^2.0.0", "axios": "^1.13.2", "cookie-parser": "^1.4.7", + "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", - "mongoose": "^8.19.3" + "mongoose": "^8.19.3", + "ngrok": "^5.0.0-beta.2", + "nodemailer": "^7.0.11", + "redis": "^5.9.0" } } diff --git a/backend/routes/auth.js b/backend/routes/auth.js index a4a246f..06c3218 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,14 +1,61 @@ import express from "express"; import redirectUser from "../controllers/auth/redirect.js"; import exchangeToken from "../controllers/auth/exchangecode.js"; -import refreshTokenController from "../controllers/auth/refreshToken.js"; +import refreshTokenMiddleware from "../middlewares/refreshTokenMiddleware.js"; import logoutUser from "../controllers/auth/logoutuser.js"; import checkBlacklistedToken from "../middlewares/checkBlacklistedToken.js"; +import getUserRepos from "../controllers/auth/getUserRepos.js"; +import getRepoPullRequests from "../controllers/auth/getRepoPullRequests.js"; +import paginationMiddleware from "../middlewares/pagination.js"; const authRouter = express.Router(); +import rateLimiter from "../middlewares/ratelimiter.js"; +import callback from "../controllers/auth/callback.js"; -authRouter.get("/redirect", redirectUser); -authRouter.get("/exchange/:code", checkBlacklistedToken, exchangeToken); -authRouter.post("/refresh", checkBlacklistedToken, refreshTokenController); +authRouter.get("/redirect", rateLimiter, redirectUser); +authRouter.get("/exchange/:code", exchangeToken); authRouter.get("/logout", checkBlacklistedToken, logoutUser); +authRouter.get("/callback", callback); -export default authRouter; \ No newline at end of file +// Secured endpoints: require not-blacklisted + refresh middleware to attach req.user +authRouter.get( + "/repos", + checkBlacklistedToken, + refreshTokenMiddleware, + paginationMiddleware, + getUserRepos +); +// Single param `repo` — owner inferred from authenticated user +authRouter.get( + "/repos/:repo/pulls", + checkBlacklistedToken, + refreshTokenMiddleware, + paginationMiddleware, + getRepoPullRequests +); + +authRouter.get( + "/status", + rateLimiter, + checkBlacklistedToken, + refreshTokenMiddleware, + (req, res) => { + const { + ghAccessToken, + ghRefreshToken, + ghAccessTokenExpiresAt, + ghRefreshTokenExpiresAt, + refreshTokenExpiresAt, + createdAt, + updatedAt, + __v, + ...safeUser + } = req.user.toObject ? req.user.toObject() : req.user; + + res.status(200).json({ + success: true, + message: "Authenticated", + user: safeUser, + }); + } +); +export default authRouter; diff --git a/backend/routes/llm.js b/backend/routes/llm.js new file mode 100644 index 0000000..67008c4 --- /dev/null +++ b/backend/routes/llm.js @@ -0,0 +1,5 @@ +import express from "express" +import analysePr from "../controllers/llm/analysePr.js" +const llmRouter = express.Router() +llmRouter.post("/analysePr/:owner/:repo/:prNumber",analysePr) ; +export default llmRouter \ No newline at end of file diff --git a/backend/routes/webhook.js b/backend/routes/webhook.js new file mode 100644 index 0000000..1c4276d --- /dev/null +++ b/backend/routes/webhook.js @@ -0,0 +1,9 @@ +import express from "express"; // import express to create a router +import handleWebhook from "../controllers/webhook/handleWebhook.js"; // import the handler that will verify and process the webhook + +const router = express.Router(); // create a new router instance + +// Use express.raw so the handler receives the raw bytes for signature verification +router.post("/", express.raw({ type: "*/*" }), handleWebhook); + +export default router; // export the configured router diff --git a/backend/server.js b/backend/server.js index ec0d559..45c693d 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,18 +1,40 @@ -import express from "express" ; +import express, { urlencoded } from "express" ; import { connectDB } from "./config/mongodbconfig.js"; import dotenv from "dotenv" import authRouter from "./routes/auth.js"; +import llmRouter from "./routes/llm.js"; +import webhookRouter from "./routes/webhook.js"; import cookieParser from "cookie-parser"; +import redisConnect from "./config/redisConnect.js"; +import cors from "cors" const app = express() ; app.use(cookieParser()) -app.use(express.json()) ; + +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +app.use(cors()) ; +app.use(cors({ + origin: "http://localhost:5173", + credentials: true, +})); dotenv.config() ; +app.use(express.urlencoded({extended:true, verify: (req, res, buf) => { req.rawBody = buf; } })) ; +app.use(cors({ + origin: process.env.FRONTEND_URL||"http://localhost:5173", + credentials:true, +})) app.use("/auth", authRouter) ; -connectDB(). -then(()=>{ - app.listen(3000, ()=>{ - console.log("Server is running on port 3000") ; - }) -}).catch((error)=>{ - console.error("Error connecting to MongoDB:", error); -}) +app.use("/llm", llmRouter) ; +app.use("/webhook", webhookRouter) ; +const promises = [connectDB(),redisConnect()] ; +async function start() { + try{ + await Promise.all(promises) ; + const PORT = process.env.PORT || 3000 ; + app.listen(PORT,()=>console.log("Server started at port 3000")) ; + }catch(err){ + console.log(err) ; + } +} +start() ; \ No newline at end of file diff --git a/backend/utils/cleandiff.js b/backend/utils/cleandiff.js new file mode 100644 index 0000000..0383c5d --- /dev/null +++ b/backend/utils/cleandiff.js @@ -0,0 +1,65 @@ +function cleanDiff(diff) { + const lines = diff.split("\n"); + const cleaned = []; + let contextCount = 0; + const MAX_CONTEXT_LINES = 3; // Limit context lines + + for (let line of lines) { + // Skip all metadata + if (line.startsWith("diff --git") || line.startsWith("index ") || + line.startsWith("--- ") || line.startsWith("+++ ")) continue; + + // Skip binary files completely + if (line.includes("Binary file")) continue; + + // Skip generated files more aggressively + if (line.includes("package-lock.json") || line.includes("yarn.lock") || + line.includes("pnpm-lock.yaml") || line.includes("dist/") || + line.includes("build/") || line.includes("node_modules/") || + line.includes(".min.js") || line.includes(".bundle.js")) { + continue; + } + + // Handle +/- lines + if (line.startsWith("+") || line.startsWith("-")) { + // More aggressive comment removal + line = line + .replace(/\/\/.*$/g, "") + .replace(/#.*$/g, "") + .replace(/\/\*[\s\S]*?\*\//g, "") + .replace(//g, "") + .replace(/".*?"/g, '""') // Remove string literals + .replace(/'.*?'/g, "''") + .trim(); + + if (line === "+" || line === "-" || line.length <= 2) continue; + + // Truncate very long lines + if (line.length > 200) { + line = line.substring(0, 200) + "..."; + } + + cleaned.push(line); + contextCount = 0; + continue; + } + + // Limit context lines between changes + if (line.startsWith(" ")) { + if (contextCount < MAX_CONTEXT_LINES && line.trim().length > 0) { + cleaned.push(line); + contextCount++; + } + continue; + } + + // Keep only essential hunk headers + if (line.startsWith("@@")) { + cleaned.push(line); + contextCount = 0; + } + } + + return cleaned.join("\n"); +} +export default cleanDiff \ No newline at end of file diff --git a/backend/utils/compressllm.js b/backend/utils/compressllm.js new file mode 100644 index 0000000..89dda74 --- /dev/null +++ b/backend/utils/compressllm.js @@ -0,0 +1,15 @@ +// Compress the final payload +function compressForLLM(response) { + return { + t: response.title?.substring(0, 200), // truncate title + a: response.author, + // Only include critical file changes + f: response.changedFiles + .filter(f => !f.includes('test') && !f.includes('spec')) + .slice(0, 30), + // Compress diff by removing whitespace + diff: response.diff.replace(/\n{3,}/g, '\n\n').substring(0, 100000) // 100KB max + }; +} + +export default compressForLLM; \ No newline at end of file diff --git a/backend/utils/decryptGithubToken.js b/backend/utils/decryptGithubToken.js index 02e80c4..60f968d 100644 --- a/backend/utils/decryptGithubToken.js +++ b/backend/utils/decryptGithubToken.js @@ -2,13 +2,12 @@ import jwt from "jsonwebtoken"; import UserModel from "../models/user/userSchema.js"; // original token nikalne k liye bas user id ke sath call jkrna h -export default async function getDecryptedGithubToken(userId) { - const user = await UserModel.findOne({ userId }); - if (!user || !user.accessToken) return null; +export default async function getDecryptedGithubToken(accesstoken) { + try { - const decoded = jwt.verify(user.accessToken, process.env.JWT_SECRET_KEY); - return decoded.access_token; + const decoded = jwt.verify(accesstoken, process.env.JWT_SECRET_KEY); + return decoded; } catch (err) { return null; } -} \ No newline at end of file +} diff --git a/backend/utils/filetypefiltering.js b/backend/utils/filetypefiltering.js new file mode 100644 index 0000000..c11a378 --- /dev/null +++ b/backend/utils/filetypefiltering.js @@ -0,0 +1,20 @@ + +// Add this before processing files +function shouldIncludeFile(filename) { + const excludedExtensions = [ + '.lock', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', + '.woff', '.woff2', '.ttf', '.eot', '.min.js', '.min.css', + '.map', '.log', '.pdf', '.zip', '.tar', '.gz' + ]; + + const excludedPaths = [ + 'dist/', 'build/', 'node_modules/', '.git/', 'coverage/', + '.nyc_output/', 'tmp/', 'temp/', 'vendor/', 'public/assets/' + ]; + + const ext = filename.substring(filename.lastIndexOf('.')); + return !excludedExtensions.includes(ext) && + !excludedPaths.some(path => filename.includes(path)); +} + +export default shouldIncludeFile \ No newline at end of file diff --git a/backend/utils/sendEmail.js b/backend/utils/sendEmail.js new file mode 100644 index 0000000..bf335a1 --- /dev/null +++ b/backend/utils/sendEmail.js @@ -0,0 +1,23 @@ +import nodemailer from "nodemailer"; + +// Create a transporter object using SMTP transport +const transporter = nodemailer.createTransport({ + service: "gmail", // You can use other services like 'hotmail', 'yahoo', etc. + auth: { + user: process.env.EMAIL_USER, // Your email address from .env + pass: process.env.EMAIL_PASS, // Your email app password from .env + }, +}); + +export default async function sendEmail({ to, subject, text, html }) { + const mailOptions = { + from: `"PullShark" <${process.env.EMAIL_USER}>`, // sender address + to: to, // list of receivers + subject: subject, // Subject line + text: text, // plain text body + html: html, // html body + }; + + await transporter.sendMail(mailOptions); + console.log(`Email sent to ${to} with subject "${subject}"`); +} \ No newline at end of file diff --git a/backend/utils/smartfile.js b/backend/utils/smartfile.js new file mode 100644 index 0000000..2f6020f --- /dev/null +++ b/backend/utils/smartfile.js @@ -0,0 +1,19 @@ +import shouldIncludeFile from "./filetypefiltering.js"; +// Prioritize important files +function prioritizeFiles(files) { + return files + .filter(f => shouldIncludeFile(f.filename)) + .sort((a, b) => { + const priorityExtensions = ['.js', '.ts', '.py', '.java', '.cpp', '.c', '.go', '.rs']; + const aExt = a.filename.substring(a.filename.lastIndexOf('.')); + const bExt = b.filename.substring(b.filename.lastIndexOf('.')); + + const aScore = priorityExtensions.includes(aExt) ? 1 : 0; + const bScore = priorityExtensions.includes(bExt) ? 1 : 0; + + return bScore - aScore; + }) + .slice(0, 50); // Limit to top 50 files +} + +export default prioritizeFiles; \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf3..3b0b403 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 4b1ea89..98f78ce 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,7 +5,7 @@ -Loading...
+ //
- Edit src/App.jsx and save to test HMR
-
- Click on the Vite and React logos to learn more -
- > - ) ++ Turbocharged reviews for engineering teams who move fast — but ship with confidence. +
+ + {/* CTA Buttons */} +14-day free trial • No credit card needed
+2-click signup with GitHub/GitLab
+ + {/* Features Section */} +{f.desc}
++ {user?.username} +
+ {user?.email && ( ++ {user.email} +
+ )} ++ Inspects code behavior, test flows, and runtime conditions, uncovering edge cases developers miss during QA. It flags unstable logic and risky interactions, before they ever reach staging or production. +
+ ++ Built for engineering teams who demand stability, predictable performance, and failure-proof releases, without hidden risks slipping into production. +
+ +{f.desc}
++ PullShark is built from the ground up with privacy, encryption, and secure AI processing at its core. +
+ +{box.desc}
+Loading repositories...
++ Manage and select repositories for code review +
++ {repo.owner?.login} +
++ {repo.description} +
+ )} + ++ You don't have any repositories yet. +
++// Join us with GitHub and start reviewing code instantly +//
+ +// {error && ( +//+// {error} +//
+// )} + +// + +//+// Already have an account?{" "} +// +// Login +// +//
+//