7 changed files with 1347 additions and 22 deletions
@ -0,0 +1,614 @@
@@ -0,0 +1,614 @@
|
||||
/** |
||||
* Git Platform Fetcher Service |
||||
*
|
||||
* Fetches issues, pull requests, and comments from external git platforms |
||||
* (GitHub, GitLab, Gitea, Codeberg, Forgejo, OneDev, custom) |
||||
* for display in the universal dashboard. |
||||
*/ |
||||
|
||||
import logger from '../logger.js'; |
||||
import type { MessagingPreferences } from '../messaging/preferences-storage.js'; |
||||
import { getPreferences } from '../messaging/preferences-storage.js'; |
||||
|
||||
type GitPlatform = 'github' | 'gitlab' | 'gitea' | 'codeberg' | 'forgejo' | 'onedev' | 'custom'; |
||||
|
||||
interface GitPlatformConfig { |
||||
baseUrl: string; |
||||
issuesPath: string; |
||||
pullsPath: string; |
||||
commentsPath: string; |
||||
authHeader: 'Bearer' | 'token'; |
||||
customHeaders?: Record<string, string>; |
||||
} |
||||
|
||||
interface ExternalIssue { |
||||
id: string | number; |
||||
number?: number; |
||||
title: string; |
||||
body: string; |
||||
state: 'open' | 'closed' | 'merged'; |
||||
created_at: string; |
||||
updated_at: string; |
||||
user: { |
||||
login?: string; |
||||
username?: string; |
||||
avatar_url?: string; |
||||
}; |
||||
html_url: string; |
||||
comments_url?: string; |
||||
comments_count?: number; |
||||
labels?: Array<{ name: string; color?: string }>; |
||||
platform: GitPlatform; |
||||
owner: string; |
||||
repo: string; |
||||
apiUrl?: string; |
||||
} |
||||
|
||||
interface ExternalPullRequest extends ExternalIssue { |
||||
head?: { |
||||
ref: string; |
||||
sha: string; |
||||
}; |
||||
base?: { |
||||
ref: string; |
||||
sha: string; |
||||
}; |
||||
merged_at?: string | null; |
||||
mergeable?: boolean; |
||||
} |
||||
|
||||
interface ExternalComment { |
||||
id: string | number; |
||||
body: string; |
||||
created_at: string; |
||||
updated_at: string; |
||||
user: { |
||||
login?: string; |
||||
username?: string; |
||||
avatar_url?: string; |
||||
}; |
||||
html_url: string; |
||||
issue_url?: string; |
||||
pull_request_url?: string; |
||||
} |
||||
|
||||
// Platform configurations
|
||||
const GIT_PLATFORM_CONFIGS: Record<string, Omit<GitPlatformConfig, 'baseUrl'>> = { |
||||
github: { |
||||
issuesPath: '/repos/{owner}/{repo}/issues', |
||||
pullsPath: '/repos/{owner}/{repo}/pulls', |
||||
commentsPath: '/repos/{owner}/{repo}/issues/{issue_number}/comments', |
||||
authHeader: 'Bearer', |
||||
customHeaders: { 'Accept': 'application/vnd.github.v3+json' } |
||||
}, |
||||
gitlab: { |
||||
issuesPath: '/projects/{owner}%2F{repo}/issues', |
||||
pullsPath: '/projects/{owner}%2F{repo}/merge_requests', |
||||
commentsPath: '/projects/{owner}%2F{repo}/issues/{issue_id}/notes', |
||||
authHeader: 'Bearer' |
||||
}, |
||||
gitea: { |
||||
issuesPath: '/repos/{owner}/{repo}/issues', |
||||
pullsPath: '/repos/{owner}/{repo}/pulls', |
||||
commentsPath: '/repos/{owner}/{repo}/issues/{issue_index}/comments', |
||||
authHeader: 'token' |
||||
}, |
||||
codeberg: { |
||||
issuesPath: '/repos/{owner}/{repo}/issues', |
||||
pullsPath: '/repos/{owner}/{repo}/pulls', |
||||
commentsPath: '/repos/{owner}/{repo}/issues/{issue_index}/comments', |
||||
authHeader: 'token' |
||||
}, |
||||
forgejo: { |
||||
issuesPath: '/repos/{owner}/{repo}/issues', |
||||
pullsPath: '/repos/{owner}/{repo}/pulls', |
||||
commentsPath: '/repos/{owner}/{repo}/issues/{issue_index}/comments', |
||||
authHeader: 'token' |
||||
}, |
||||
onedev: { |
||||
issuesPath: '/{owner}/{repo}/issues', |
||||
pullsPath: '/{owner}/{repo}/pull-requests', |
||||
commentsPath: '/{owner}/{repo}/issues/{issue_id}/comments', |
||||
authHeader: 'Bearer' |
||||
}, |
||||
custom: { |
||||
issuesPath: '/repos/{owner}/{repo}/issues', |
||||
pullsPath: '/repos/{owner}/{repo}/pulls', |
||||
commentsPath: '/repos/{owner}/{repo}/issues/{issue_number}/comments', |
||||
authHeader: 'Bearer' |
||||
} |
||||
}; |
||||
|
||||
function getGitPlatformConfig( |
||||
platform: GitPlatform, |
||||
customApiUrl?: string |
||||
): GitPlatformConfig { |
||||
if (platform === 'onedev') { |
||||
if (!customApiUrl) { |
||||
throw new Error('OneDev requires apiUrl to be provided'); |
||||
} |
||||
return { |
||||
...GIT_PLATFORM_CONFIGS.onedev, |
||||
baseUrl: customApiUrl |
||||
}; |
||||
} |
||||
|
||||
if (platform === 'gitea' || platform === 'forgejo') { |
||||
const config = GIT_PLATFORM_CONFIGS[platform]; |
||||
const baseUrls: Record<string, string> = { |
||||
gitea: customApiUrl || 'https://codeberg.org/api/v1', |
||||
forgejo: customApiUrl || 'https://forgejo.org/api/v1' |
||||
}; |
||||
return { |
||||
...config, |
||||
baseUrl: baseUrls[platform] |
||||
}; |
||||
} |
||||
|
||||
if (platform === 'custom') { |
||||
if (!customApiUrl) { |
||||
throw new Error('Custom platform requires apiUrl'); |
||||
} |
||||
return { |
||||
...GIT_PLATFORM_CONFIGS.custom, |
||||
baseUrl: customApiUrl |
||||
}; |
||||
} |
||||
|
||||
if (customApiUrl) { |
||||
const config = GIT_PLATFORM_CONFIGS[platform]; |
||||
return { |
||||
...config, |
||||
baseUrl: customApiUrl |
||||
}; |
||||
} |
||||
|
||||
const config = GIT_PLATFORM_CONFIGS[platform]; |
||||
const baseUrls: Record<string, string> = { |
||||
github: 'https://api.github.com', |
||||
gitlab: 'https://gitlab.com/api/v4', |
||||
codeberg: 'https://codeberg.org/api/v1' |
||||
}; |
||||
|
||||
return { |
||||
...config, |
||||
baseUrl: baseUrls[platform] || '' |
||||
}; |
||||
} |
||||
|
||||
function buildUrl( |
||||
config: GitPlatformConfig, |
||||
path: string, |
||||
owner: string, |
||||
repo: string, |
||||
platform: GitPlatform, |
||||
params?: Record<string, string | number> |
||||
): string { |
||||
let urlPath = path |
||||
.replace('{owner}', encodeURIComponent(owner)) |
||||
.replace('{repo}', encodeURIComponent(repo)); |
||||
|
||||
if (platform === 'gitlab') { |
||||
// GitLab uses URL-encoded owner/repo
|
||||
urlPath = path.replace('{owner}%2F{repo}', encodeURIComponent(`${owner}/${repo}`)); |
||||
} |
||||
|
||||
if (platform === 'onedev') { |
||||
// OneDev uses project-path format: /api/projects/{project-path}/issues
|
||||
const projectPath = repo ? `${owner}/${repo}` : owner; |
||||
urlPath = path.replace('{owner}/{repo}', encodeURIComponent(projectPath)); |
||||
// OneDev paths are relative to /api/projects/ (baseUrl already includes /api/projects)
|
||||
// But we need to add it here since path is relative
|
||||
if (!urlPath.startsWith('/api/projects')) { |
||||
urlPath = `/api/projects${urlPath}`; |
||||
} |
||||
} |
||||
|
||||
// Replace path parameters
|
||||
if (params) { |
||||
for (const [key, value] of Object.entries(params)) { |
||||
urlPath = urlPath.replace(`{${key}}`, String(value)); |
||||
} |
||||
} |
||||
|
||||
const url = `${config.baseUrl}${urlPath}`; |
||||
const searchParams = new URLSearchParams(); |
||||
searchParams.set('state', 'all'); // Get both open and closed
|
||||
searchParams.set('per_page', '50'); // Limit results
|
||||
searchParams.set('sort', 'updated'); |
||||
searchParams.set('direction', 'desc'); |
||||
|
||||
return `${url}?${searchParams.toString()}`; |
||||
} |
||||
|
||||
function buildAuthHeader(config: GitPlatformConfig, token: string): string { |
||||
return config.authHeader === 'Bearer' ? `Bearer ${token}` : `token ${token}`; |
||||
} |
||||
|
||||
async function fetchFromPlatform<T>( |
||||
url: string, |
||||
headers: Record<string, string>, |
||||
platform: string |
||||
): Promise<T[]> { |
||||
try { |
||||
const response = await fetch(url, { headers }); |
||||
|
||||
if (!response.ok) { |
||||
if (response.status === 404) { |
||||
return []; // Repository or resource not found
|
||||
} |
||||
const error = await response.json().catch(() => ({ message: response.statusText })); |
||||
throw new Error(`${platform} API error: ${error.message || response.statusText}`); |
||||
} |
||||
|
||||
const data = await response.json(); |
||||
return Array.isArray(data) ? data : [data]; |
||||
} catch (error) { |
||||
logger.error({ error, url: url.slice(0, 100) + '...', platform }, 'Failed to fetch from git platform'); |
||||
return []; |
||||
} |
||||
} |
||||
|
||||
function normalizeIssue( |
||||
raw: any, |
||||
platform: GitPlatform, |
||||
owner: string, |
||||
repo: string, |
||||
apiUrl?: string |
||||
): ExternalIssue { |
||||
// GitHub format
|
||||
if (raw.number !== undefined) { |
||||
return { |
||||
id: raw.id || raw.number, |
||||
number: raw.number, |
||||
title: raw.title || '', |
||||
body: raw.body || '', |
||||
state: raw.state === 'closed' ? 'closed' : 'open', |
||||
created_at: raw.created_at || '', |
||||
updated_at: raw.updated_at || '', |
||||
user: { |
||||
login: raw.user?.login, |
||||
avatar_url: raw.user?.avatar_url |
||||
}, |
||||
html_url: raw.html_url || '', |
||||
comments_url: raw.comments_url, |
||||
comments_count: raw.comments, |
||||
labels: raw.labels?.map((l: any) => ({ name: l.name, color: l.color })), |
||||
platform, |
||||
owner, |
||||
repo, |
||||
apiUrl |
||||
}; |
||||
} |
||||
|
||||
// GitLab format
|
||||
if (raw.iid !== undefined) { |
||||
const baseUrl = apiUrl || (platform === 'gitlab' ? 'https://gitlab.com' : ''); |
||||
return { |
||||
id: raw.id || raw.iid, |
||||
number: raw.iid, |
||||
title: raw.title || '', |
||||
body: raw.description || '', |
||||
state: raw.state === 'closed' ? 'closed' : 'open', |
||||
created_at: raw.created_at || '', |
||||
updated_at: raw.updated_at || '', |
||||
user: { |
||||
username: raw.author?.username, |
||||
avatar_url: raw.author?.avatar_url |
||||
}, |
||||
html_url: raw.web_url || `${baseUrl}/${owner}/${repo}/-/issues/${raw.iid}`, |
||||
comments_count: raw.user_notes_count, |
||||
labels: raw.labels?.map((l: string) => ({ name: l })), |
||||
platform, |
||||
owner, |
||||
repo, |
||||
apiUrl |
||||
}; |
||||
} |
||||
|
||||
// Gitea/Codeberg/Forgejo format
|
||||
if (raw.index !== undefined) { |
||||
const baseUrl = apiUrl || (platform === 'codeberg' ? 'https://codeberg.org' : ''); |
||||
return { |
||||
id: raw.id || raw.index, |
||||
number: raw.index, |
||||
title: raw.title || '', |
||||
body: raw.body || '', |
||||
state: raw.state === 'closed' ? 'closed' : 'open', |
||||
created_at: raw.created_at || '', |
||||
updated_at: raw.updated_at || '', |
||||
user: { |
||||
username: raw.user?.username || raw.poster?.username, |
||||
avatar_url: raw.user?.avatar_url || raw.poster?.avatar_url |
||||
}, |
||||
html_url: raw.html_url || `${baseUrl}/${owner}/${repo}/issues/${raw.index}`, |
||||
comments_count: raw.comments, |
||||
labels: raw.labels?.map((l: any) => ({ name: typeof l === 'string' ? l : l.name })), |
||||
platform, |
||||
owner, |
||||
repo, |
||||
apiUrl |
||||
}; |
||||
} |
||||
|
||||
// OneDev format
|
||||
if (platform === 'onedev') { |
||||
const baseUrl = apiUrl || ''; |
||||
return { |
||||
id: raw.id || raw.number, |
||||
number: raw.number, |
||||
title: raw.title || '', |
||||
body: raw.description || '', |
||||
state: raw.state === 'closed' ? 'closed' : 'open', |
||||
created_at: raw.submitDate || '', |
||||
updated_at: raw.updateDate || '', |
||||
user: { |
||||
username: raw.submitter?.name, |
||||
avatar_url: raw.submitter?.avatarUrl |
||||
}, |
||||
html_url: raw.url || `${baseUrl}/${owner}/${repo}/issues/${raw.number}`, |
||||
platform, |
||||
owner, |
||||
repo, |
||||
apiUrl |
||||
}; |
||||
} |
||||
|
||||
// Fallback
|
||||
return { |
||||
id: raw.id || raw.number || 0, |
||||
number: raw.number, |
||||
title: raw.title || '', |
||||
body: raw.body || raw.description || '', |
||||
state: 'open', |
||||
created_at: raw.created_at || '', |
||||
updated_at: raw.updated_at || '', |
||||
user: {}, |
||||
html_url: raw.html_url || raw.web_url || '', |
||||
platform, |
||||
owner, |
||||
repo, |
||||
apiUrl |
||||
}; |
||||
} |
||||
|
||||
function normalizePullRequest( |
||||
raw: any, |
||||
platform: GitPlatform, |
||||
owner: string, |
||||
repo: string, |
||||
apiUrl?: string |
||||
): ExternalPullRequest { |
||||
const issue = normalizeIssue(raw, platform, owner, repo, apiUrl); |
||||
|
||||
// GitHub format
|
||||
if (raw.head && raw.base) { |
||||
return { |
||||
...issue, |
||||
state: raw.merged ? 'merged' : (raw.state === 'closed' ? 'closed' : 'open'), |
||||
head: { |
||||
ref: raw.head.ref, |
||||
sha: raw.head.sha |
||||
}, |
||||
base: { |
||||
ref: raw.base.ref, |
||||
sha: raw.base.sha |
||||
}, |
||||
merged_at: raw.merged_at, |
||||
mergeable: raw.mergeable |
||||
}; |
||||
} |
||||
|
||||
// GitLab format
|
||||
if (raw.source_branch && raw.target_branch) { |
||||
return { |
||||
...issue, |
||||
state: raw.state === 'merged' ? 'merged' : (raw.state === 'closed' ? 'closed' : 'open'), |
||||
head: { |
||||
ref: raw.source_branch, |
||||
sha: raw.sha || '' |
||||
}, |
||||
base: { |
||||
ref: raw.target_branch, |
||||
sha: '' |
||||
}, |
||||
merged_at: raw.merged_at |
||||
}; |
||||
} |
||||
|
||||
// Gitea/Codeberg/Forgejo format
|
||||
if (raw.head && typeof raw.head === 'object') { |
||||
return { |
||||
...issue, |
||||
state: raw.state === 'closed' ? 'closed' : 'open', |
||||
head: { |
||||
ref: raw.head.ref || raw.head.name || '', |
||||
sha: raw.head.sha || '' |
||||
}, |
||||
base: { |
||||
ref: raw.base?.ref || raw.base?.name || '', |
||||
sha: raw.base?.sha || '' |
||||
} |
||||
}; |
||||
} |
||||
|
||||
return issue as ExternalPullRequest; |
||||
} |
||||
|
||||
/** |
||||
* Fetch issues from a git platform |
||||
*/ |
||||
async function fetchIssues( |
||||
platform: GitPlatform, |
||||
owner: string, |
||||
repo: string, |
||||
token: string, |
||||
apiUrl?: string |
||||
): Promise<ExternalIssue[]> { |
||||
try { |
||||
const config = getGitPlatformConfig(platform, apiUrl); |
||||
const url = buildUrl(config, config.issuesPath, owner, repo, platform); |
||||
const headers: Record<string, string> = { |
||||
'Authorization': buildAuthHeader(config, token), |
||||
'Content-Type': 'application/json', |
||||
'User-Agent': 'GitRepublic', |
||||
...(config.customHeaders || {}) |
||||
}; |
||||
|
||||
const rawIssues = await fetchFromPlatform<any>(url, headers, platform); |
||||
return rawIssues |
||||
.filter((issue: any) => !issue.pull_request) // Exclude PRs (GitHub returns PRs in issues endpoint)
|
||||
.map((issue: any) => normalizeIssue(issue, platform, owner, repo, apiUrl)); |
||||
} catch (error) { |
||||
logger.error({ error, platform, owner, repo }, 'Failed to fetch issues'); |
||||
return []; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Fetch pull requests from a git platform |
||||
*/ |
||||
async function fetchPullRequests( |
||||
platform: GitPlatform, |
||||
owner: string, |
||||
repo: string, |
||||
token: string, |
||||
apiUrl?: string |
||||
): Promise<ExternalPullRequest[]> { |
||||
try { |
||||
const config = getGitPlatformConfig(platform, apiUrl); |
||||
const url = buildUrl(config, config.pullsPath, owner, repo, platform); |
||||
const headers: Record<string, string> = { |
||||
'Authorization': buildAuthHeader(config, token), |
||||
'Content-Type': 'application/json', |
||||
'User-Agent': 'GitRepublic', |
||||
...(config.customHeaders || {}) |
||||
}; |
||||
|
||||
const rawPRs = await fetchFromPlatform<any>(url, headers, platform); |
||||
return rawPRs.map((pr: any) => normalizePullRequest(pr, platform, owner, repo, apiUrl)); |
||||
} catch (error) { |
||||
logger.error({ error, platform, owner, repo }, 'Failed to fetch pull requests'); |
||||
return []; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Fetch comments for an issue or PR |
||||
*/ |
||||
async function fetchComments( |
||||
platform: GitPlatform, |
||||
owner: string, |
||||
repo: string, |
||||
issueNumber: number, |
||||
token: string, |
||||
apiUrl?: string |
||||
): Promise<ExternalComment[]> { |
||||
try { |
||||
const config = getGitPlatformConfig(platform, apiUrl); |
||||
const commentsPath = config.commentsPath.replace('{issue_number}', String(issueNumber)) |
||||
.replace('{issue_id}', String(issueNumber)) |
||||
.replace('{issue_index}', String(issueNumber)); |
||||
const url = buildUrl(config, commentsPath, owner, repo, platform); |
||||
const headers: Record<string, string> = { |
||||
'Authorization': buildAuthHeader(config, token), |
||||
'Content-Type': 'application/json', |
||||
'User-Agent': 'GitRepublic', |
||||
...(config.customHeaders || {}) |
||||
}; |
||||
|
||||
const rawComments = await fetchFromPlatform<any>(url, headers, platform); |
||||
return rawComments.map((comment: any) => ({ |
||||
id: comment.id, |
||||
body: comment.body || comment.note || '', |
||||
created_at: comment.created_at || '', |
||||
updated_at: comment.updated_at || '', |
||||
user: { |
||||
login: comment.user?.login || comment.author?.username, |
||||
username: comment.user?.username || comment.author?.username, |
||||
avatar_url: comment.user?.avatar_url || comment.author?.avatar_url |
||||
}, |
||||
html_url: comment.html_url || comment.url || '', |
||||
issue_url: comment.issue_url, |
||||
pull_request_url: comment.pull_request_url |
||||
})); |
||||
} catch (error) { |
||||
logger.error({ error, platform, owner, repo, issueNumber }, 'Failed to fetch comments'); |
||||
return []; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Get all issues and PRs from user's configured git platforms |
||||
*/ |
||||
export async function getAllExternalItems( |
||||
userPubkeyHex: string |
||||
): Promise<{ |
||||
issues: ExternalIssue[]; |
||||
pullRequests: ExternalPullRequest[]; |
||||
}> { |
||||
const preferences = await getPreferences(userPubkeyHex); |
||||
if (!preferences || !preferences.gitPlatforms || preferences.gitPlatforms.length === 0) { |
||||
return { issues: [], pullRequests: [] }; |
||||
} |
||||
|
||||
const allIssues: ExternalIssue[] = []; |
||||
const allPRs: ExternalPullRequest[] = []; |
||||
|
||||
// Fetch from all configured platforms in parallel
|
||||
const promises: Promise<void>[] = []; |
||||
|
||||
for (const gitPlatform of preferences.gitPlatforms) { |
||||
if (!gitPlatform.owner || !gitPlatform.repo || !gitPlatform.token) { |
||||
continue; |
||||
} |
||||
|
||||
if (gitPlatform.platform === 'onedev' && !gitPlatform.apiUrl) { |
||||
logger.warn({ platform: 'onedev' }, 'OneDev requires apiUrl'); |
||||
continue; |
||||
} |
||||
|
||||
if (gitPlatform.platform === 'custom' && !gitPlatform.apiUrl) { |
||||
logger.warn({ platform: 'custom' }, 'Custom platform requires apiUrl'); |
||||
continue; |
||||
} |
||||
|
||||
promises.push( |
||||
fetchIssues( |
||||
gitPlatform.platform, |
||||
gitPlatform.owner, |
||||
gitPlatform.repo, |
||||
gitPlatform.token, |
||||
gitPlatform.apiUrl |
||||
).then(issues => { |
||||
allIssues.push(...issues); |
||||
}).catch(err => { |
||||
logger.warn({ error: err, platform: gitPlatform.platform }, 'Failed to fetch issues'); |
||||
}) |
||||
); |
||||
|
||||
promises.push( |
||||
fetchPullRequests( |
||||
gitPlatform.platform, |
||||
gitPlatform.owner, |
||||
gitPlatform.repo, |
||||
gitPlatform.token, |
||||
gitPlatform.apiUrl |
||||
).then(prs => { |
||||
allPRs.push(...prs); |
||||
}).catch(err => { |
||||
logger.warn({ error: err, platform: gitPlatform.platform }, 'Failed to fetch PRs'); |
||||
}) |
||||
); |
||||
} |
||||
|
||||
await Promise.allSettled(promises); |
||||
|
||||
// Sort by updated_at descending
|
||||
allIssues.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()); |
||||
allPRs.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()); |
||||
|
||||
return { issues: allIssues, pullRequests: allPRs }; |
||||
} |
||||
|
||||
export type { ExternalIssue, ExternalPullRequest, ExternalComment, GitPlatform }; |
||||
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
/** |
||||
* Git Dashboard API |
||||
*
|
||||
* Aggregates issues and pull requests from external git platforms |
||||
* for display in the universal dashboard. |
||||
*/ |
||||
|
||||
import { json, error } from '@sveltejs/kit'; |
||||
import type { RequestHandler } from './$types'; |
||||
import { extractRequestContext } from '$lib/utils/api-context.js'; |
||||
import { getAllExternalItems } from '$lib/services/git-platforms/git-platform-fetcher.js'; |
||||
import { getCachedUserLevel } from '$lib/services/security/user-level-cache.js'; |
||||
import logger from '$lib/services/logger.js'; |
||||
|
||||
/** |
||||
* GET /api/user/git-dashboard |
||||
* Get aggregated issues and PRs from all configured git platforms |
||||
*/ |
||||
export const GET: RequestHandler = async (event) => { |
||||
const requestContext = extractRequestContext(event); |
||||
const clientIp = requestContext.clientIp || 'unknown'; |
||||
|
||||
try { |
||||
if (!requestContext.userPubkeyHex) { |
||||
return error(401, 'Authentication required'); |
||||
} |
||||
|
||||
// Check user has unlimited access (same requirement as messaging forwarding)
|
||||
const userLevel = getCachedUserLevel(requestContext.userPubkeyHex); |
||||
if (!userLevel || userLevel.level !== 'unlimited') { |
||||
return json({ |
||||
issues: [], |
||||
pullRequests: [], |
||||
message: 'Git dashboard requires unlimited access. Please verify you can write to at least one default Nostr relay.' |
||||
}); |
||||
} |
||||
|
||||
const { issues, pullRequests } = await getAllExternalItems(requestContext.userPubkeyHex); |
||||
|
||||
logger.debug({ |
||||
userPubkey: requestContext.userPubkeyHex.slice(0, 16) + '...', |
||||
issuesCount: issues.length, |
||||
prsCount: pullRequests.length, |
||||
clientIp |
||||
}, 'Fetched git dashboard data'); |
||||
|
||||
return json({ |
||||
issues, |
||||
pullRequests |
||||
}); |
||||
} catch (e) { |
||||
logger.error({ error: e, clientIp }, 'Failed to fetch git dashboard data'); |
||||
return error(500, 'Failed to fetch git dashboard data'); |
||||
} |
||||
}; |
||||
Loading…
Reference in new issue