Skip to content

Commit b71c2fb

Browse files
committed
feat(github-api): implement GitHub API proxy routes for fetching and submitting feedback, update configuration for internal API usage
1 parent 7328b3f commit b71c2fb

14 files changed

Lines changed: 224 additions & 192 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { NextResponse } from 'next/server';
2+
3+
const GITHUB_BASE_URL = 'https://api.github.com';
4+
const USER_AGENT = process.env.GITHUB_API_USER_AGENT || 'SortVision-App';
5+
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
6+
7+
function buildHeaders() {
8+
const headers = {
9+
Accept: 'application/vnd.github+json',
10+
'User-Agent': USER_AGENT,
11+
};
12+
if (GITHUB_TOKEN) {
13+
headers.Authorization = `Bearer ${GITHUB_TOKEN}`;
14+
}
15+
return headers;
16+
}
17+
18+
export async function GET(request, context) {
19+
const resolvedParams = await context?.params;
20+
const pathSegments = resolvedParams?.path || [];
21+
if (!Array.isArray(pathSegments) || pathSegments.length === 0) {
22+
return NextResponse.json({ error: 'Missing GitHub path' }, { status: 400 });
23+
}
24+
25+
const url = new URL(`${GITHUB_BASE_URL}/${pathSegments.join('/')}`);
26+
request.nextUrl.searchParams.forEach((value, key) => {
27+
url.searchParams.set(key, value);
28+
});
29+
30+
try {
31+
const upstream = await fetch(url.toString(), {
32+
method: 'GET',
33+
headers: buildHeaders(),
34+
cache: 'no-store',
35+
});
36+
37+
const text = await upstream.text();
38+
const response = new NextResponse(text, {
39+
status: upstream.status,
40+
headers: {
41+
'Content-Type':
42+
upstream.headers.get('content-type') || 'application/json',
43+
},
44+
});
45+
46+
const passHeaders = [
47+
'x-ratelimit-limit',
48+
'x-ratelimit-remaining',
49+
'x-ratelimit-reset',
50+
];
51+
for (const headerName of passHeaders) {
52+
const headerValue = upstream.headers.get(headerName);
53+
if (headerValue) response.headers.set(headerName, headerValue);
54+
}
55+
56+
return response;
57+
} catch (error) {
58+
return NextResponse.json(
59+
{
60+
error: 'Failed to proxy GitHub API request',
61+
message: error instanceof Error ? error.message : 'Unknown error',
62+
},
63+
{ status: 500 }
64+
);
65+
}
66+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { NextResponse } from 'next/server';
2+
import { buildGitHubFeedbackIssue } from '@/components/feedback/services/github/buildGitHubFeedbackIssue';
3+
4+
const GITHUB_BASE_URL = 'https://api.github.com';
5+
const USER_AGENT = process.env.GITHUB_API_USER_AGENT || 'SortVision-App';
6+
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
7+
const REPO_OWNER =
8+
process.env.FEEDBACK_REPO_OWNER ||
9+
process.env.NEXT_PUBLIC_FEEDBACK_REPO_OWNER ||
10+
'alienx5499';
11+
const REPO_NAME =
12+
process.env.FEEDBACK_REPO_NAME ||
13+
process.env.NEXT_PUBLIC_FEEDBACK_REPO_NAME ||
14+
'SortVision';
15+
16+
export async function POST(request) {
17+
if (!GITHUB_TOKEN) {
18+
return NextResponse.json(
19+
{ error: 'Missing server GitHub token (GITHUB_TOKEN)' },
20+
{ status: 500 }
21+
);
22+
}
23+
24+
try {
25+
const feedbackData = await request.json();
26+
const issueData = buildGitHubFeedbackIssue(feedbackData);
27+
28+
const upstream = await fetch(
29+
`${GITHUB_BASE_URL}/repos/${REPO_OWNER}/${REPO_NAME}/issues`,
30+
{
31+
method: 'POST',
32+
headers: {
33+
Accept: 'application/vnd.github+json',
34+
'User-Agent': USER_AGENT,
35+
Authorization: `Bearer ${GITHUB_TOKEN}`,
36+
'Content-Type': 'application/json',
37+
},
38+
body: JSON.stringify(issueData),
39+
}
40+
);
41+
42+
const payload = await upstream.json().catch(() => ({}));
43+
if (!upstream.ok) {
44+
return NextResponse.json(
45+
{
46+
error: 'GitHub feedback submission failed',
47+
status: upstream.status,
48+
message: payload?.message || upstream.statusText,
49+
},
50+
{ status: upstream.status }
51+
);
52+
}
53+
54+
return NextResponse.json({
55+
success: true,
56+
issueNumber: payload.number,
57+
issueUrl: payload.html_url,
58+
data: payload,
59+
});
60+
} catch (error) {
61+
return NextResponse.json(
62+
{
63+
error: 'Invalid feedback payload',
64+
message: error instanceof Error ? error.message : 'Unknown error',
65+
},
66+
{ status: 400 }
67+
);
68+
}
69+
}

SortVision/src/components/feedback/services/github/buildGitHubFeedbackIssue.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ ${formatErrorHistory(feedbackData.errorHistory)}
6464
- **Source:** SortVision Feedback Form
6565
- **Environment:** ${DEV_MODE ? 'Development' : 'Production'}
6666
- **Session ID:** \`${feedbackData.sessionData?.sessionId || 'Unknown'}\`
67-
- **User Agent:** ${feedbackData.sessionData?.userAgent || navigator.userAgent}
67+
- **User Agent:** ${
68+
feedbackData.sessionData?.userAgent ||
69+
globalThis?.navigator?.userAgent ||
70+
'Unknown'
71+
}
6872
- **Page URL:** ${
6973
typeof window !== 'undefined' ? window.location.href : 'Unknown'
7074
}

SortVision/src/components/feedback/services/github/githubApiClient.js

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,29 @@
33
*/
44

55
import {
6-
GITHUB_API_BASE,
76
REPO_OWNER,
87
REPO_NAME,
9-
USER_AGENT,
108
ENABLE_API_LOGGING,
119
} from './githubFeedbackConfig';
1210

13-
export function githubHeaders(token) {
14-
return {
15-
Authorization: `token ${token}`,
16-
Accept: 'application/vnd.github.v3+json',
17-
'User-Agent': USER_AGENT,
18-
};
11+
export function githubHeaders() {
12+
return { Accept: 'application/json' };
1913
}
2014

2115
/**
2216
* Validate GitHub token and repository access
2317
* @returns {Promise<boolean>} - Whether the token is valid
2418
*/
2519
export async function validateGitHubAccess() {
26-
const token = process.env.NEXT_PUBLIC_GITHUB_TOKEN;
27-
28-
if (!token || !REPO_OWNER) {
20+
if (!REPO_OWNER) {
2921
return false;
3022
}
3123

3224
try {
3325
const response = await fetch(
34-
`${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}`,
26+
`/api/github/repos/${REPO_OWNER}/${REPO_NAME}`,
3527
{
36-
headers: githubHeaders(token),
28+
headers: githubHeaders(),
3729
}
3830
);
3931

@@ -58,17 +50,15 @@ export async function validateGitHubAccess() {
5850
* @returns {Promise<Object|null>} - Repository information
5951
*/
6052
export async function getRepoInfo() {
61-
const token = process.env.NEXT_PUBLIC_GITHUB_TOKEN;
62-
63-
if (!token || !REPO_OWNER) {
53+
if (!REPO_OWNER) {
6454
return null;
6555
}
6656

6757
try {
6858
const response = await fetch(
69-
`${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}`,
59+
`/api/github/repos/${REPO_OWNER}/${REPO_NAME}`,
7060
{
71-
headers: githubHeaders(token),
61+
headers: githubHeaders(),
7262
}
7363
);
7464

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
export const GITHUB_API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
2-
export const REPO_OWNER = process.env.NEXT_PUBLIC_FEEDBACK_REPO_OWNER;
3-
export const REPO_NAME = process.env.NEXT_PUBLIC_FEEDBACK_REPO_NAME;
4-
export const USER_AGENT = process.env.NEXT_PUBLIC_API_USER_AGENT;
1+
export const GITHUB_API_BASE = '/api/github';
2+
export const REPO_OWNER =
3+
process.env.NEXT_PUBLIC_FEEDBACK_REPO_OWNER || 'alienx5499';
4+
export const REPO_NAME =
5+
process.env.NEXT_PUBLIC_FEEDBACK_REPO_NAME || 'SortVision';
56
export const DEV_MODE = process.env.NEXT_PUBLIC_DEV_MODE === 'true';
67
export const ENABLE_API_LOGGING =
78
process.env.NEXT_PUBLIC_ENABLE_API_LOGGING === 'true' || DEV_MODE;

SortVision/src/components/feedback/services/github/githubService.js

Lines changed: 11 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,25 @@
22
* GitHub API service for SortVision feedback submission
33
*/
44

5-
import {
6-
GITHUB_API_BASE,
7-
REPO_OWNER,
8-
REPO_NAME,
9-
DEV_MODE,
10-
ENABLE_API_LOGGING,
11-
} from './githubFeedbackConfig';
12-
import { githubHeaders } from './githubApiClient';
13-
import { buildGitHubFeedbackIssue } from './buildGitHubFeedbackIssue';
5+
import { DEV_MODE, ENABLE_API_LOGGING } from './githubFeedbackConfig';
146

157
export { validateGitHubAccess, getRepoInfo } from './githubApiClient';
168

17-
function validateFeedbackGitHubConfig(token) {
18-
if (!token) {
19-
const message =
20-
'GitHub token not found. Please set NEXT_PUBLIC_GITHUB_TOKEN in your environment variables.';
21-
console.error(message);
22-
throw new Error(message);
23-
}
24-
25-
if (!REPO_OWNER) {
26-
const message =
27-
'Repository owner missing. Please set NEXT_PUBLIC_GITHUB_REPO_OWNER in your environment variables.';
28-
console.error(message);
29-
throw new Error(message);
30-
}
31-
}
32-
339
function logIfEnabled(message, data) {
3410
if (ENABLE_API_LOGGING) {
3511
console.log(message, data);
3612
}
3713
}
3814

39-
function mapGitHubErrorStatus(responseStatus, repoOwner, repoName) {
15+
function mapGitHubErrorStatus(responseStatus) {
4016
if (responseStatus === 404) {
41-
return `Repository '${repoOwner}/${repoName}' not found or token lacks access. Verify: 1) Repository exists 2) Token has 'repo' scope 3) Token has access to private repos`;
17+
return 'Feedback API endpoint or repository was not found.';
4218
}
4319
if (responseStatus === 401) {
44-
return 'GitHub token is invalid or expired. Please check your NEXT_PUBLIC_GITHUB_TOKEN.';
20+
return 'Feedback API is unauthorized. Check server GitHub token.';
4521
}
4622
if (responseStatus === 403) {
47-
return 'GitHub token lacks required permissions. Ensure token has "repo" and "issues" scopes.';
23+
return 'Feedback API is forbidden. Verify server token scopes.';
4824
}
4925
return null;
5026
}
@@ -55,42 +31,26 @@ function mapGitHubErrorStatus(responseStatus, repoOwner, repoName) {
5531
* @returns {Promise<Object>} - Response from GitHub API
5632
*/
5733
export const submitFeedback = async feedbackData => {
58-
const token = process.env.NEXT_PUBLIC_GITHUB_TOKEN;
59-
validateFeedbackGitHubConfig(token);
60-
6134
logIfEnabled('GitHub API Debug Info:', {
62-
apiBase: GITHUB_API_BASE,
63-
repoOwner: REPO_OWNER,
64-
repoName: REPO_NAME,
65-
tokenPresent: !!token,
66-
tokenPrefix: token ? `${token.substring(0, 8)}...` : 'None',
35+
endpoint: '/api/github/feedback',
36+
clientSideTokenPresent: false,
6737
environment: DEV_MODE ? 'Development' : 'Production',
6838
});
6939

70-
const issueData = buildGitHubFeedbackIssue(feedbackData);
71-
logIfEnabled('Submitting feedback to GitHub:', {
72-
repo: `${REPO_OWNER}/${REPO_NAME}`,
73-
title: issueData.title,
74-
labels: issueData.labels,
75-
});
76-
7740
try {
78-
const apiUrl = `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues`;
41+
const apiUrl = '/api/github/feedback';
7942

8043
logIfEnabled('Making GitHub API request:', {
8144
url: apiUrl,
8245
method: 'POST',
83-
hasToken: !!token,
84-
tokenLength: token ? token.length : 0,
8546
});
8647

8748
const response = await fetch(apiUrl, {
8849
method: 'POST',
8950
headers: {
90-
...githubHeaders(token),
9151
'Content-Type': 'application/json',
9252
},
93-
body: JSON.stringify(issueData),
53+
body: JSON.stringify(feedbackData),
9454
});
9555

9656
if (!response.ok) {
@@ -111,16 +71,11 @@ export const submitFeedback = async feedbackData => {
11171
status: response.status,
11272
statusText: response.statusText,
11373
url: apiUrl,
114-
repoOwner: REPO_OWNER,
115-
repoName: REPO_NAME,
74+
repo: 'server-managed',
11675
errorData: errorData,
11776
});
11877

119-
const statusMessage = mapGitHubErrorStatus(
120-
response.status,
121-
REPO_OWNER,
122-
REPO_NAME
123-
);
78+
const statusMessage = mapGitHubErrorStatus(response.status);
12479
if (statusMessage) {
12580
throw new Error(statusMessage);
12681
}

0 commit comments

Comments
 (0)