-
Notifications
You must be signed in to change notification settings - Fork 0
github #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
github #87
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| # Performance Audit Todo List | ||
|
|
||
| ## Frontend (React/Vite) | ||
| - [ ] Analyze Vite bundle size (e.g., using `rollup-plugin-visualizer`). | ||
| - [ ] Implement lazy loading (`React.lazy`) for heavy routes and components. | ||
| - [ ] Audit React component re-renders and apply `useMemo`/`useCallback` where appropriate. | ||
| - [ ] Optimize static assets (images, fonts, large icons) for faster loading. | ||
| - [ ] Run Lighthouse audits on the frontend production build. | ||
| - [ ] Review and clean up unnecessary dependencies in `package.json`. | ||
|
|
||
| ## Backend (Node/Express) | ||
| - [ ] Review MongoDB collection structures and add necessary indexes for slow queries. | ||
| - [ ] Optimize database queries (avoid resolving unnecessary large nested documents). | ||
| - [x] Implement API pagination and limit dataset sizes returned to the client. | ||
| - [ ] Introduce caching strategies (e.g., Redis) for frequently accessed, strictly read-only data. | ||
| - [ ] Profile expensive API routes and optimize heavy computational logic. | ||
| - [ ] Audit WebSocket (socket.io) event payloads and frequency to prevent network saturation. | ||
|
|
||
| ## Network & Infrastructure | ||
| - [ ] Verify HTTP gzip/brotli compression is enabled on the server. | ||
| - [ ] Setup application performance monitoring (APM) to track response times. | ||
| - [ ] Review Docker image sizes and optimize `Dockerfile` build processes. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ const CryptoJS = require('crypto-js'); | |
| const User = require('../models/User'); | ||
| const verifyToken = require('../middleware/authMiddleware'); | ||
| const { normalizeDoc } = require('../utils/normalize'); | ||
| const { getInstallationAccessToken } = require('../utils/githubAppAuth'); | ||
|
|
||
| const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY; | ||
|
|
||
|
|
@@ -21,6 +22,28 @@ const decryptToken = (ciphertext) => { | |
| return bytes.toString(CryptoJS.enc.Utf8); | ||
| }; | ||
|
|
||
| const githubCache = new Map(); | ||
|
|
||
| const fetchWithEtag = async (url, config, cacheKey) => { | ||
| const cached = githubCache.get(cacheKey); | ||
| const headers = { ...config.headers }; | ||
| if (cached && cached.etag) { | ||
| headers['If-None-Match'] = cached.etag; | ||
| } | ||
|
|
||
| try { | ||
| const res = await axios.get(url, { ...config, headers }); | ||
| if (res.headers?.etag) { | ||
| githubCache.set(cacheKey, { etag: res.headers.etag, data: res.data }); | ||
| } | ||
| return res; | ||
| } catch (error) { | ||
| if (error.response && error.response.status === 304 && cached) { | ||
| return { ...error.response, status: 304, data: cached.data }; | ||
| } | ||
| throw error; | ||
| } | ||
| }; | ||
|
|
||
| router.post('/connect', verifyToken, async (req, res) => { | ||
| const { accessToken } = req.body; | ||
|
|
@@ -31,12 +54,16 @@ router.post('/connect', verifyToken, async (req, res) => { | |
| } | ||
|
|
||
| try { | ||
| const githubResponse = await axios.get('https://api.github.com/user', { | ||
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| Accept: 'application/vnd.github.v3+json' | ||
| } | ||
| }); | ||
| const githubResponse = await fetchWithEtag( | ||
| 'https://api.github.com/user', | ||
| { | ||
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| Accept: 'application/vnd.github.v3+json' | ||
| } | ||
| }, | ||
| `connect_user_${uid}` | ||
| ); | ||
|
|
||
| const githubUsername = githubResponse.data.login; | ||
| const encryptedToken = encryptToken(accessToken); | ||
|
|
@@ -138,12 +165,16 @@ router.post('/callback', verifyToken, async (req, res) => { | |
| return res.status(400).json({ message: error_description || 'Failed to exchange code for token' }); | ||
| } | ||
|
|
||
| const userResponse = await axios.get('https://api.github.com/user', { | ||
| headers: { | ||
| Authorization: `Bearer ${access_token}`, | ||
| Accept: 'application/vnd.github.v3+json' | ||
| } | ||
| }); | ||
| const userResponse = await fetchWithEtag( | ||
| 'https://api.github.com/user', | ||
| { | ||
| headers: { | ||
| Authorization: `Bearer ${access_token}`, | ||
| Accept: 'application/vnd.github.v3+json' | ||
| } | ||
| }, | ||
| `callback_user_${uid}` | ||
| ); | ||
|
|
||
| const githubUser = userResponse.data; | ||
| const encryptedToken = encryptToken(access_token); | ||
|
|
@@ -197,19 +228,27 @@ router.get('/repos', verifyToken, async (req, res) => { | |
| return res.status(500).json({ message: 'Invalid stored token' }); | ||
| } | ||
|
|
||
| const githubResponse = await axios.get('https://api.github.com/user/repos', { | ||
| const page = parseInt(req.query.page) || 1; | ||
| const per_page = parseInt(req.query.per_page) || 30; | ||
|
|
||
| const cacheKey = `repos_${uid}_${page}_${per_page}`; | ||
| const githubResponse = await fetchWithEtag('https://api.github.com/user/repos', { | ||
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| Accept: 'application/vnd.github.v3+json' | ||
| }, | ||
| params: { | ||
| sort: 'updated', | ||
| visibility: 'all', | ||
| per_page: 100 | ||
| per_page, | ||
| page | ||
| } | ||
| }); | ||
| }, cacheKey); | ||
|
|
||
| const linkHeader = githubResponse.headers.link; | ||
| const hasNextPage = !!(linkHeader && linkHeader.includes('rel="next"')); | ||
|
|
||
| res.json(githubResponse.data); | ||
| res.json({ repos: githubResponse.data, hasNextPage, page }); | ||
|
|
||
| } catch (error) { | ||
| console.error('Error fetching GitHub repos:', error.message); | ||
|
|
@@ -348,7 +387,29 @@ router.get('/user-repos', verifyToken, async (req, res) => { | |
| } | ||
|
|
||
| try { | ||
| const response = await octokit.request('GET /installation/repositories', { per_page: 100 }); | ||
| const page = parseInt(req.query.page) || 1; | ||
| const per_page = parseInt(req.query.per_page) || 30; | ||
|
|
||
| const cacheKey = `user_repos_${uid}_${page}_${per_page}`; | ||
| const cached = githubCache.get(cacheKey); | ||
| const headers = cached && cached.etag ? { 'If-None-Match': cached.etag } : {}; | ||
|
|
||
| let response; | ||
| try { | ||
| response = await octokit.request('GET /installation/repositories', { per_page, page, headers }); | ||
| if (response.headers?.etag) { | ||
| githubCache.set(cacheKey, { etag: response.headers.etag, data: response.data }); | ||
| } | ||
| } catch (err) { | ||
| if (err.status === 304 && cached) { | ||
| response = { headers: err.response?.headers || {}, data: cached.data }; | ||
| } else { | ||
| throw err; | ||
| } | ||
| } | ||
|
|
||
| const linkHeader = response.headers?.link; | ||
| const hasNextPage = !!(linkHeader && linkHeader.includes('rel="next"')); | ||
|
|
||
| const repos = response.data.repositories.map(repo => ({ | ||
| id: repo.id, | ||
|
|
@@ -359,7 +420,7 @@ router.get('/user-repos', verifyToken, async (req, res) => { | |
| html_url: repo.html_url | ||
| })); | ||
|
|
||
| res.json(repos); | ||
| res.json({ repos, hasNextPage, page }); | ||
| } catch (requestErr) { | ||
| console.error("Error fetching repositories from GitHub:", requestErr.message); | ||
| await disconnectGithub(uid, { installationId: null }); | ||
|
|
@@ -401,12 +462,13 @@ router.get('/stats', verifyToken, async (req, res) => { | |
| const accessToken = decryptToken(github.accessToken); | ||
| const username = github.username; | ||
|
|
||
| const userResponse = await axios.get(`https://api.github.com/users/${username}`, { | ||
| const cacheKey = `stats_${username}`; | ||
| const userResponse = await fetchWithEtag(`https://api.github.com/users/${username}`, { | ||
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| Accept: 'application/vnd.github.v3+json' | ||
| } | ||
| }); | ||
| }, cacheKey); | ||
|
|
||
| res.json({ | ||
| login: userResponse.data.login, | ||
|
|
@@ -440,12 +502,13 @@ router.get('/events', verifyToken, async (req, res) => { | |
| const accessToken = decryptToken(github.accessToken); | ||
| const username = github.username; | ||
|
|
||
| const eventsResponse = await axios.get(`https://api.github.com/users/${username}/events?per_page=30`, { | ||
| const cacheKey = `events_${username}`; | ||
| const eventsResponse = await fetchWithEtag(`https://api.github.com/users/${username}/events?per_page=30`, { | ||
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| Accept: 'application/vnd.github.v3+json' | ||
| } | ||
| }); | ||
| }, cacheKey); | ||
|
|
||
| const events = eventsResponse.data.map(event => ({ | ||
| id: event.id, | ||
|
|
@@ -561,22 +624,24 @@ router.get('/readme', verifyToken, async (req, res) => { | |
| } | ||
|
|
||
| const installationId = github.installationId; | ||
| const appId = process.env.GITHUB_APP_ID; | ||
| let privateKey = process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, '\n'); | ||
|
|
||
| const { App } = await import('octokit'); | ||
| const app = new App({ appId, privateKey }); | ||
| const octokit = await app.getInstallationOctokit(parseInt(installationId)); | ||
| const accessToken = await getInstallationAccessToken(installationId); | ||
| const readmeCacheKey = `readme_${uid}_${owner}_${repo}_${installationId}`; | ||
|
|
||
|
Comment on lines
+628
to
629
|
||
| try { | ||
| const response = await octokit.request('GET /repos/{owner}/{repo}/readme', { | ||
| owner, | ||
| repo, | ||
| mediaType: { format: 'raw' } | ||
| }); | ||
| const response = await fetchWithEtag( | ||
| `https://api.github.com/repos/${owner}/${repo}/readme`, | ||
| { | ||
|
Comment on lines
+631
to
+633
|
||
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| Accept: 'application/vnd.github.raw+json' | ||
| } | ||
| }, | ||
| readmeCacheKey | ||
| ); | ||
|
|
||
| res.send(response.data); | ||
| } catch (err) { | ||
| if (err.status === 404) { | ||
| if (err.response && err.response.status === 404) { | ||
| return res.send("# No README found"); | ||
| } | ||
| throw err; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getInstallationAccessToken()ultimately reads the GitHub App private key fromprocess.env.GITHUB_APP_PRIVATE_KEY, but the repo’s env template/documentation usesGITHUB_PRIVATE_KEY. As-is, this route can crash at runtime when generating the JWT. Align the env var name (or add fallback) and update.env.exampleaccordingly.