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.

Result:

${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 @@ - frontend + PullShark
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d4c0152..7e79cd9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,9 +8,13 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@reduxjs/toolkit": "^2.10.1", "@tailwindcss/vite": "^4.1.17", + "axios": "^1.13.2", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-redux": "^9.2.0", + "react-router-dom": "^7.9.5", "tailwindcss": "^4.1.17" }, "devDependencies": { @@ -977,6 +981,32 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz", + "integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.2.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.43", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", @@ -1270,6 +1300,18 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@tailwindcss/node": { "version": "4.1.17", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", @@ -1589,7 +1631,7 @@ "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1605,6 +1647,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz", @@ -1689,6 +1737,23 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1751,6 +1816,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "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", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1819,6 +1897,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1833,6 +1923,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1852,7 +1951,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -1880,6 +1979,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1889,6 +1997,20 @@ "node": ">=8" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.249", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.249.tgz", @@ -1909,6 +2031,51 @@ "node": ">=10.13.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", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -2239,6 +2406,42 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2253,6 +2456,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2263,6 +2475,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2289,6 +2538,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2305,6 +2566,45 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2315,6 +2615,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -2389,9 +2699,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -2763,6 +3073,36 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2954,6 +3294,12 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2985,6 +3331,29 @@ "react": "^19.2.0" } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -2995,6 +3364,65 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz", + "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.5.tgz", + "integrity": "sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.5" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3062,6 +3490,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3209,6 +3643,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5bb4acb..ee03138 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,9 +10,13 @@ "preview": "vite preview" }, "dependencies": { + "@reduxjs/toolkit": "^2.10.1", "@tailwindcss/vite": "^4.1.17", + "axios": "^1.13.2", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-redux": "^9.2.0", + "react-router-dom": "^7.9.5", "tailwindcss": "^4.1.17" }, "devDependencies": { diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 083ced4..e41378e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,35 +1,57 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import { useEffect, useState } from 'react' +import { Routes, Route } from "react-router"; +import { useDispatch, useSelector } from "react-redux"; +import Header from "./components/Header"; +import Footer from "./components/Footer"; +import Home from "./pages/Home"; +import Login from "./pages/Login"; +import Signup from './pages/Signup'; +import RepoPage from './pages/RepoPage'; +import ScrollToHash from './components/ScrollToHash'; +import AuthCallback from './components/AuthCallback'; +import { checkAuthStatusThunk } from "./slice/authSlice"; function App() { - const [count, setCount] = useState(0) + const dispatch = useDispatch(); + const [appLoading, setAppLoading] = useState(true); + + + useEffect(() => { + const init = async () => { + dispatch(checkAuthStatusThunk()); + setAppLoading(false); + }; + init(); +  }, []); + // Show loading until auth check is complete + // if (appLoading) { + // return ( + //
+ //
+ //
+ //

Loading...

+ //
+ //
+ // ); + // } return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.jsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) +
+
+
+ + + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+ ); } export default App diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js new file mode 100644 index 0000000..ec9b484 --- /dev/null +++ b/frontend/src/api/auth.js @@ -0,0 +1,31 @@ +import axios from "axios"; + +export const api = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL, + withCredentials: true, // send/receive cookies +}); + +// 1) Start GitHub login (redirects the browser) +export async function startGithubLogin() { + await api.get("/auth/redirect"); +} + +// 2) Handle GitHub OAuth callback: /auth/callback?code=... +export async function exchangeCodeForSession(code) { + const { data } = await api.get(`/auth/exchange/${code}`); + // -> { success, message, data: { username, email, userId } } + return data; +} + +// 3) Check current session +export const checkAuthStatus = async () => { + const res = await api.get("/auth/status", { withCredentials: true }); + return res.data; +}; + +// 4) Logout +export async function logout() { + const { data } = await api.get("/auth/logout"); + // -> { success: true, message: "Logged out successfully" } + return data; +} \ No newline at end of file diff --git a/frontend/src/components/.gitignore b/frontend/src/components/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/components/AuthCallback.jsx b/frontend/src/components/AuthCallback.jsx new file mode 100644 index 0000000..462fdef --- /dev/null +++ b/frontend/src/components/AuthCallback.jsx @@ -0,0 +1,34 @@ +import { useEffect } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { useDispatch } from "react-redux"; +import { exchangeCodeThunk } from "../slice/authSlice"; + +export default function AuthCallback() { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + + useEffect(() => { + const code = searchParams.get("code"); + + if (!code) { + navigate("/login", { replace: true }); + return; + } + + dispatch(exchangeCodeThunk(code)) + .unwrap() + .then(() => { + navigate("/repo", { replace: true }); + }) + .catch(() => { + navigate("/login", { replace: true }); + }); + }, [dispatch, navigate, searchParams]); + + return ( +
+
Processing login...
+
+ ); +} diff --git a/frontend/src/components/Features.jsx b/frontend/src/components/Features.jsx new file mode 100644 index 0000000..54a566e --- /dev/null +++ b/frontend/src/components/Features.jsx @@ -0,0 +1,57 @@ +import React from "react"; + +function Features() { + const features = [ + { + title: "Catch fast. Fix fast.", + desc: "Full codebase-aware reviews and one-click fixes that follow your coding guidelines.", + }, + { + title: "Simple PR summaries.", + desc: "View changed files and one-line descriptions for easy review.", + }, + ]; + + return ( +
+ {/* Header Text */} +

+ Cut Review Time. Catch More Bugs. +

+ +

+ Turbocharged reviews for engineering teams who move fast — but ship with confidence. +

+ + {/* CTA Buttons */} +
+ + + +
+ + {/* Trust Text */} +

14-day free trial • No credit card needed

+

2-click signup with GitHub/GitLab

+ + {/* Features Section */} +
+ {features.map((f, i) => ( +
+

{f.title}

+

{f.desc}

+
+ ))} +
+
+ ); +} + +export default Features \ No newline at end of file diff --git a/frontend/src/components/Footer.jsx b/frontend/src/components/Footer.jsx new file mode 100644 index 0000000..d213897 --- /dev/null +++ b/frontend/src/components/Footer.jsx @@ -0,0 +1,27 @@ +import React from 'react' + +function Footer() { + return ( + + ) +} + +export default Footer \ No newline at end of file diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx new file mode 100644 index 0000000..559caec --- /dev/null +++ b/frontend/src/components/Header.jsx @@ -0,0 +1,165 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Link, useNavigate } from "react-router"; +import { useSelector, useDispatch } from "react-redux"; +import { logoutThunk } from "../slice/authSlice"; + +function Header() { + const { loading, user } = useSelector((state) => state.auth); + const dispatch = useDispatch(); + const navigate = useNavigate(); + + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setIsDropdownOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const handleLogout = () => { + dispatch(logoutThunk()).then(() => { + navigate("/"); + }); + }; + + // Safe initials extraction + const getInitials = () => { + if (!user?.username) return "U"; + const name = user.username.trim(); + if (!name) return "U"; + + const parts = name.split(" "); + + if (parts.length === 1) { + return parts[0][0].toUpperCase(); + } + + return (parts[0][0] + parts[1][0]).toUpperCase(); + }; + + const initials = getInitials(); + + // 🔥 Avatar Shimmer Component + const AvatarShimmer = () => ( +
+ ); + + return ( +
+
+ + {/* LEFT LOGO */} +
+
+ + + + +
+ PullShark AI +
+ + {/* NAV LINKS */} + + + {/* RIGHT SIDE CONTENT */} +
+ + {/* 🔹 1. SHOW SHIMMER WHILE AUTH LOADING */} + {loading && ( +
+ +
+
+ )} + + {/* 🔹 2. NOT LOGGED IN */} + {!loading && !user && ( + + + + )} + + {/* 🔹 3. LOGGED IN */} + {!loading && user && ( +
+ + + {/* DROPDOWN */} + {isDropdownOpen && ( +
+
+

+ {user?.username} +

+ {user?.email && ( +

+ {user.email} +

+ )} +
+ + setIsDropdownOpen(false)} + > + My Repositories + + + +
+ )} +
+ )} +
+
+
+ ); +} + +export default Header; diff --git a/frontend/src/components/Hero.jsx b/frontend/src/components/Hero.jsx new file mode 100644 index 0000000..ab89f1c --- /dev/null +++ b/frontend/src/components/Hero.jsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { Link } from 'react-router'; + +function Hero() { + return ( +
+
+
+ {/* LEFT CONTENT */} +
+ + Next-Gen AI Code Intelligence + + +

+ Automatically Detect Hidden Edge Cases + + Before They Break Your Builds + +

+ +

+ 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. +

+ +
+ + + + + +
+ +
+ + ⚡ Faster • 🤖 Smarter • 🔒 Secure + +
+
+ + {/* RIGHT ILLUSTRATION */} +
+
+ {/* Animated floating gradient blob */} +
+ + + + + + + + + + +
+ + {/* Highlight line */} +
+
+
+
+
+
+ ); +} + +export default Hero \ No newline at end of file diff --git a/frontend/src/components/HowItWork.jsx b/frontend/src/components/HowItWork.jsx new file mode 100644 index 0000000..f350066 --- /dev/null +++ b/frontend/src/components/HowItWork.jsx @@ -0,0 +1,56 @@ +import React from 'react'; + +function HowItWork() { + const items = [ + { + title: "SaaS Mode", + points: [ + "Install PullShark with one click on GitHub or GitLab.", + "Configure integrations like Jira, Linear, or Slack (optional).", + "Create or update a PR — AI reviews appear instantly.", + "Enjoy automatic scaling, updates, and zero maintenance.", + ], + }, + { + title: "Self-Hosted Mode", + points: [ + "Deploy PullShark on your servers or private cloud.", + "Keep full control over data pipelines and compliance.", + "Integrate enterprise-grade IAM, SSO, and internal tooling.", + ], + }, + ]; + + return ( +
+
+

+ How PullShark Works +

+ +
+ {items.map((block, i) => ( +
+

+ {block.title} +

+
    + {block.points.map((p, idx) => ( +
  • + + {p} +
  • + ))} +
+
+ ))} +
+
+
+ ); +} + +export default HowItWork \ No newline at end of file diff --git a/frontend/src/components/ProfuctFeatures.jsx b/frontend/src/components/ProfuctFeatures.jsx new file mode 100644 index 0000000..7fa3cb9 --- /dev/null +++ b/frontend/src/components/ProfuctFeatures.jsx @@ -0,0 +1,60 @@ +import React from "react"; + +function ProductFeatures() { + const features = [ + { + title: "Adaptive Edge-Case Engine", + desc: "AI that learns your system’s behavior and automatically identifies unusual conditions, rare flows, and hidden failure patterns.", + }, + { + title: "Real-Time Failure Prediction", + desc: "Detect instability before it becomes a bug — with continuous monitoring of states, constraints, and behavioral anomalies.", + }, + { + title: "Environment-Aware Analysis", + desc: "Understand how your product behaves across networks, devices, and scenarios through deep multi-condition modeling.", + }, + { + title: "High-Signal Insights, Zero Noise", + desc: "Only actionable, meaningful alerts. No clutter, no false positives — just precise detection of reliability risks.", + }, + { + title: "AI-Driven Test Intelligence", + desc: "Generate smart test cases that target edge conditions, system boundaries, and potential failure triggers automatically.", + }, + { + title: "Full-Stack Stability Mapping", + desc: "Visualize risk zones, dependency impacts, and weak points across your system to proactively strengthen reliability.", + }, + ] + + return ( +
+ +
+

+ AI-Powered Edge Case & Reliability Platform +

+

+ Built for engineering teams who demand stability, predictable performance, and failure-proof releases, without hidden risks slipping into production. +

+ +
+ {features.map((f, idx) => ( +
+

+ {f.title} +

+

{f.desc}

+
+ ))} +
+
+
+ ); +} + +export default ProductFeatures; diff --git a/frontend/src/components/ScrollToHash.jsx b/frontend/src/components/ScrollToHash.jsx new file mode 100644 index 0000000..0c8a1f3 --- /dev/null +++ b/frontend/src/components/ScrollToHash.jsx @@ -0,0 +1,15 @@ +import { useEffect } from "react"; +import { useLocation } from "react-router-dom"; + +export default function ScrollToHash() { + const { hash } = useLocation(); + + useEffect(() => { + if (hash) { + const el = document.querySelector(hash); + if (el) el.scrollIntoView({ behavior: "smooth" }); + } + }, [hash]); + + return null; +} \ No newline at end of file diff --git a/frontend/src/components/Security.jsx b/frontend/src/components/Security.jsx new file mode 100644 index 0000000..f4ec08d --- /dev/null +++ b/frontend/src/components/Security.jsx @@ -0,0 +1,53 @@ +import React from 'react'; + +function Security() { + const cards = [ + { + title: "Ephemeral Review Sessions", + desc: "AI reviews run in isolated, temporary environments — no logs, no retention, no footprints.", + }, + { + title: "Zero-Visibility Encryption", + desc: "Your data stays encrypted at rest and in transit with automated secure deletion post-analysis.", + }, + { + title: "Enterprise-Grade Compliance", + desc: "SOC 2 Type II certified workflows with strict access controls and continuous auditing.", + }, + ]; + + return ( +
+
+

+ Your Data. Fully Protected. +

+

+ PullShark is built from the ground up with privacy, encryption, and secure AI processing at its core. +

+ +
+ {cards.map((box, i) => ( +
+

+ {box.title} +

+

{box.desc}

+
+ ))} +
+ +
+ +
+
+
+ ); +} + +export default Security; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index 0b2e018..4b42d86 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,5 +1,5 @@ @import "tailwindcss"; -:root { +/* :root { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; @@ -66,4 +66,4 @@ button:focus-visible { button { background-color: #f9f9f9; } -} +} */ diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index b9a1a6d..75c60b0 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,10 +1,15 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { BrowserRouter } from "react-router"; import './index.css' import App from './App.jsx' +import { Provider } from "react-redux"; +import { store } from "./stores/index.js"; createRoot(document.getElementById('root')).render( - + + - , + + ) diff --git a/frontend/src/pages/.gitignore b/frontend/src/pages/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx new file mode 100644 index 0000000..5f9f3a6 --- /dev/null +++ b/frontend/src/pages/Home.jsx @@ -0,0 +1,27 @@ +import React from 'react' +import Hero from '../components/Hero.jsx' +import ProfuctFeatures from '../components/ProfuctFeatures.jsx'; +import Features from '../components/Features.jsx'; +import HowItWorks from '../components/HowItWork.jsx'; +import Security from '../components/Security.jsx'; + +export default function Home() { + return ( +
+
+ +
+
+ +
+
+ +
+
+ +
+ + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx new file mode 100644 index 0000000..e9290eb --- /dev/null +++ b/frontend/src/pages/Login.jsx @@ -0,0 +1,56 @@ +import { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { startGithubLoginAction, exchangeCodeThunk } from "../slice/authSlice"; +import { Link, useSearchParams, useNavigate } from "react-router-dom"; + +export default function LoginPanel() { + const dispatch = useDispatch(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const code = searchParams.get("code"); + const authenticated = useSelector((s) => s.auth.authenticated); + + useEffect(() => { + if (code) { + dispatch(exchangeCodeThunk(code)).unwrap().catch(() => navigate("/login")); + } + }, [code]); + + + useEffect(() => { + if (authenticated) { + // remove ?code from URL + window.history.replaceState({}, "", "/"); + navigate("/"); + } + }, [authenticated]); + + return ( +
+
+ +

+ Log In +

+ + + +
+ + Don’t have an account? Sign up + +
+ +
+
+ ); +} diff --git a/frontend/src/pages/RepoPage.jsx b/frontend/src/pages/RepoPage.jsx new file mode 100644 index 0000000..291cdc0 --- /dev/null +++ b/frontend/src/pages/RepoPage.jsx @@ -0,0 +1,177 @@ +import { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { fetchReposThunk } from "../slice/repoSlice"; + +export default function RepoPage() { + const dispatch = useDispatch(); + + const { + repos, + loading, + error, + pagination: { page, hasNextPage, hasPrevPage }, + } = useSelector((state) => state.repo); + + useEffect(() => { + dispatch(fetchReposThunk({ page, limit: 10 })); + }, [dispatch, page]); + + const handleNextPage = () => { + if (hasNextPage) dispatch(fetchReposThunk({ page: page + 1, limit: 10 })); + }; + + const handlePrevPage = () => { + if (hasPrevPage) dispatch(fetchReposThunk({ page: page - 1, limit: 10 })); + }; + + const formatDate = (d) => + new Date(d).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + + if (loading) { + return ( +
+
+

Loading repositories...

+
+ ); + } + + return ( +
+ + {/* Title */} +
+

+ Your Repositories +

+

+ Manage and select repositories for code review +

+
+ + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Repo Cards */} +
+ {repos.length > 0 ? ( + <> +
+ {repos.map((repo) => ( +
+ {/* Repo Header */} +
+
+ {repo.owner?.avatar_url && ( + {repo.owner.login} + )} +
+

+ {repo.name} +

+

+ {repo.owner?.login} +

+
+
+ + + {repo.private ? "Private" : "Public"} + +
+ + {/* Description */} + {repo.description && ( +

+ {repo.description} +

+ )} + +
+ Updated {formatDate(repo.updated_at)} +
+ + {/* Buttons */} +
+ + View on GitHub + + + +
+
+ ))} +
+ + {/* Pagination */} +
+ + + + Page {page} + + + +
+ + ) : ( +
+

No repositories found

+

+ You don't have any repositories yet. +

+
+ )} +
+
+ ); +} + diff --git a/frontend/src/pages/Signup.jsx b/frontend/src/pages/Signup.jsx new file mode 100644 index 0000000..af19238 --- /dev/null +++ b/frontend/src/pages/Signup.jsx @@ -0,0 +1,82 @@ +// import { useEffect } from "react"; +// import { useDispatch, useSelector } from "react-redux"; +// import { startGithubLoginAction, clearError } from "../slice/authSlice"; +// import { Link, useNavigate } from "react-router"; + +// export default function Signup() { +// const dispatch = useDispatch(); +// const navigate = useNavigate(); +// const { loading, error, authenticated } = useSelector((state) => state.auth); + +// useEffect(() => { +// if (authenticated) { +// navigate("/repo"); +// } +// }, [authenticated, navigate]); + +// useEffect(() => { +// return () => { +// dispatch(clearError()); +// }; +// }, [dispatch]); + +// const handleGithubSignup = () => { +// dispatch(startGithubLoginAction()); +// }; + +// if (authenticated) { +// return null; +// } + +// return ( +//
+//
+//
+ +//

Create Account

+//

+// Join us with GitHub and start reviewing code instantly +//

+ +// {error && ( +//

+// {error} +//

+// )} + +// + +//

+// Already have an account?{" "} +// +// Login +// +//

+//
+//
+// ); +// } +import React from 'react' + +function Signup() { + return ( +
Signup
+ ) +} + +export default Signup \ No newline at end of file diff --git a/frontend/src/pages/yblogin.jsx b/frontend/src/pages/yblogin.jsx new file mode 100644 index 0000000..e74c0fb --- /dev/null +++ b/frontend/src/pages/yblogin.jsx @@ -0,0 +1,5 @@ +export default function Login() { + return( +
Vansh bhai love you
+ ) +} \ No newline at end of file diff --git a/frontend/src/slice/.gitignore b/frontend/src/slice/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/slice/authSlice.js b/frontend/src/slice/authSlice.js new file mode 100644 index 0000000..af6ed19 --- /dev/null +++ b/frontend/src/slice/authSlice.js @@ -0,0 +1,133 @@ +import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; +import { checkAuthStatus, exchangeCodeForSession, logout } from "../api/auth"; + +// ✔ CHECK SESSION +export const checkAuthStatusThunk = createAsyncThunk( + "auth/checkStatus", + async (_, { rejectWithValue }) => { + try { + const res = await checkAuthStatus(); + // { success, authenticated, user } + return res; + } catch (e) { + return rejectWithValue( + e?.response?.data || { message: "Not authenticated" } + ); + } + } +); + +// ✔ EXCHANGE GITHUB CODE +export const exchangeCodeThunk = createAsyncThunk( + "auth/exchangeCode", + async (code, { rejectWithValue }) => { + try { + const res = await exchangeCodeForSession(code); + // res = { success, data: { username, email, userId } } + return res.data; + } catch (e) { + return rejectWithValue(e?.response?.data || { message: "Login failed" }); + } + } +); + +// ✔ LOGOUT +export const logoutThunk = createAsyncThunk( + "auth/logout", + async (_, { rejectWithValue }) => { + try { + const res = await logout(); + return res; + } catch (e) { + return rejectWithValue(e?.response?.data || { message: "Logout failed" }); + } + } +); + +const authSlice = createSlice({ + name: "auth", + initialState: { + authenticated: false, + user: null, + loading: false, + error: null, + }, + + reducers: { + startGithubLoginAction: () => { + const base = import.meta.env.VITE_API_BASE_URL; + window.location.href = `${base}/auth/redirect`; + }, + }, + + extraReducers: (builder) => { + builder + + // -------------------------- + // ✔ CHECK SESSION + // -------------------------- + .addCase(checkAuthStatusThunk.pending, (state) => { + state.loading = true; + state.error = null; + }) + + .addCase(checkAuthStatusThunk.fulfilled, (state, action) => { + state.loading = false; + state.authenticated = action.payload?.authenticated || false; + state.user = action.payload?.user || null; + state.error = null; + }) + + .addCase(checkAuthStatusThunk.rejected, (state, action) => { + state.loading = false; + state.authenticated = false; + state.user = null; + state.error = action.payload?.message || "Not authenticated"; + }) + + // -------------------------- + // ✔ EXCHANGE GITHUB CODE + // -------------------------- + .addCase(exchangeCodeThunk.pending, (state) => { + state.loading = true; + state.error = null; + }) + + .addCase(exchangeCodeThunk.fulfilled, (state, action) => { + state.loading = false; + state.authenticated = true; + state.user = action.payload; + state.error = null; + }) + + .addCase(exchangeCodeThunk.rejected, (state, action) => { + state.loading = false; + state.authenticated = false; + state.user = null; + state.error = action.payload?.message || "Login failed"; + }) + + // -------------------------- + // ✔ LOGOUT + // -------------------------- + .addCase(logoutThunk.pending, (state) => { + state.loading = true; + state.error = null; + }) + + .addCase(logoutThunk.fulfilled, (state) => { + state.loading = false; + state.authenticated = false; + state.user = null; + state.error = null; + }) + + .addCase(logoutThunk.rejected, (state, action) => { + state.loading = false; + state.error = action.payload?.message || "Logout failed"; + }); + }, +}); + +export const { startGithubLoginAction } = authSlice.actions; +export default authSlice.reducer; diff --git a/frontend/src/slice/repoSlice.js b/frontend/src/slice/repoSlice.js new file mode 100644 index 0000000..4c5b461 --- /dev/null +++ b/frontend/src/slice/repoSlice.js @@ -0,0 +1,65 @@ +import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; +import { api } from "../api/auth"; + +export const fetchReposThunk = createAsyncThunk( + "repos/fetch", + async ({ page = 1, limit = 10 }, { rejectWithValue }) => { + try { + const res = await api.get(`/repos?page=${page}&limit=${limit}`, { + withCredentials: true, + }); + return res.data; // { success, repos, pagination } + } catch (err) { + return rejectWithValue(err.response?.data || { message: "Failed to fetch repos" }); + } + } +); + +const repoSlice = createSlice({ + name: "repos", + initialState: { + repos: [], + loading: false, + error: null, + pagination: { + page: 1, + hasNextPage: false, + hasPrevPage: false, + }, + }, + + reducers: { + changePage: (state, action) => { + state.pagination.page = action.payload; + }, + }, + + extraReducers: (builder) => { + builder + .addCase(fetchReposThunk.pending, (state) => { + state.loading = true; + state.error = null; + }) + + .addCase(fetchReposThunk.fulfilled, (state, action) => { + state.loading = false; + state.repos = action.payload.repos || []; + + // Pagination + state.pagination = { + page: action.payload.pagination?.page || 1, + hasNextPage: action.payload.pagination?.hasNextPage || false, + hasPrevPage: action.payload.pagination?.hasPrevPage || false, + }; + }) + + .addCase(fetchReposThunk.rejected, (state, action) => { + state.loading = false; + state.repos = []; + state.error = action.payload?.message || "Failed to fetch repos"; + }); + }, +}); + +export const { changePage } = repoSlice.actions; +export default repoSlice.reducer; \ No newline at end of file diff --git a/frontend/src/stores/.gitignore b/frontend/src/stores/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/stores/index.js b/frontend/src/stores/index.js new file mode 100644 index 0000000..f72ab94 --- /dev/null +++ b/frontend/src/stores/index.js @@ -0,0 +1,10 @@ +import { configureStore } from "@reduxjs/toolkit"; +import authReducer from "../slice/authSlice"; +import repoReducer from "../slice/repoSlice"; + +export const store = configureStore({ + reducer: { + auth: authReducer, + repos: repoReducer, + }, +}); \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 0887120..be33248 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -5,4 +5,19 @@ import tailwindcss from '@tailwindcss/vite' export default defineConfig({ plugins: [react(),tailwindcss()], + server: { + port: 5173, + proxy: { + // Proxy API requests to your backend + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + secure: false, + }, + '/auth': { + target: 'http://localhost:3000', + changeOrigin: true, + secure: false, + } + }} }) \ No newline at end of file