Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions components/NavBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
v-if="showLogo"
class="flex-1 flex flex-row items-center h-20 border-b mx-3 md:mx-6"
>
<jabref-logo class="w-10 flex-none" />
<jabref-logo class="w-12 flex-none" />
<span
class="ml-3 flex-1 text-highlighted text-2xl font-semibold lg:inline-block hidden"
>
Expand Down Expand Up @@ -137,11 +137,30 @@
</div>
</nav>

<!-- Empty stopper for proper alignment -->
<!-- Right spacer — also holds the GitHub link to keep menu centred -->
<div
v-show="!isSmallDisplay"
class="flex-1 mx-3 md:mx-6"
/>
class="flex-1 flex items-center justify-end mx-3 md:mx-6"
>
<a
v-if="showGithubLink"
href="https://github.com/JabRef/jabref"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-1.5 text-dimmed hover:text-primary-600 transition-colors"
aria-label="JabRef on GitHub"
>
<Icon
name="ri:github-fill"
class="text-2xl"
/>
<span
v-if="stars"
class="text-sm font-medium"
>{{ stars }}</span
>
</a>
</div>
</nav>
</template>

Expand All @@ -156,7 +175,7 @@ const isHamburgerShown = ref(false)

const isSmallDisplay = useBreakpoints(breakpointsTailwind).smallerOrEqual('md')

defineProps({
const props = defineProps({
showLogo: {
type: Boolean,
default: false,
Expand All @@ -169,8 +188,16 @@ defineProps({
type: Boolean,
default: false,
},
showGithubLink: {
type: Boolean,
default: false,
},
})

const { stars } = props.showGithubLink
? useGitHubStars('JabRef/jabref')
: { stars: ref<string | null>(null) }

const { resolveClient } = useApolloClient()

const { mutate: logout, onDone } = useMutation(
Expand Down
30 changes: 30 additions & 0 deletions composables/useGitHubStars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export function useGitHubStars(repo: string) {
const stars = ref<string | null>(null)

if (import.meta.client) {
fetch(`/api/githubStars?repo=${encodeURIComponent(repo)}`)
.then((res) => {
if (!res.ok) {
throw new Error(`Failed to fetch GitHub stars: ${res.statusText}`)
}
return res.json()
})
.then((data: { stars?: number }) => {
if (data.stars !== undefined) {
stars.value = formatStarCount(data.stars)
}
})
.catch((error) => {
console.debug('Failed to fetch GitHub stars:', error)
})
}

return { stars }
}

function formatStarCount(count: number): string {
if (count >= 1000) {
return `${(count / 1000).toFixed(1).replace(/\.0$/, '')}K`
}
return count.toString()
}
15 changes: 15 additions & 0 deletions pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<NavBar
:show-search-bar="false"
:show-logo="true"
:show-github-link="true"
>
<div class="space-x-14">
<t-nuxtlink
Expand All @@ -29,6 +30,20 @@
>{{ link.title }}</t-nuxtlink
>
</li>
<li>
<a
href="https://github.com/JabRef/jabref"
target="_blank"
rel="noopener noreferrer"
class="hover:text-primary-600 font-semibold inline-flex items-center gap-1.5"
>
<Icon
name="ri:github-fill"
class="text-lg"
/>
GitHub
</a>
</li>
</ul>
</template>
</NavBar>
Expand Down
31 changes: 31 additions & 0 deletions server/api/githubStars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const repo = query.repo as string

if (!repo) {
throw createError({
statusCode: 400,
message: 'Missing repo parameter',
})
}

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repo is taken directly from the query string and interpolated into the GitHub API path without validation/normalization. Since the UI always queries a single repo, this effectively exposes a public proxy that can be abused to trigger arbitrary GitHub API calls (and burn rate limit). Prefer hardcoding the JabRef repo in the handler (no repo param), or strictly validate the parameter (e.g., owner/name with a tight regex) and reject anything else.

Suggested change
const query = getQuery(event)
const repo = query.repo as string
if (!repo) {
throw createError({
statusCode: 400,
message: 'Missing repo parameter',
})
}
const repo = 'JabRef/jabref'

Copilot uses AI. Check for mistakes.
try {
const response = await fetch(`https://api.github.com/repos/${repo}`)

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint uses unauthenticated GitHub REST API requests, which are heavily rate-limited (and may fail intermittently under load). The codebase already has runtimeConfig.githubRepoToken used for GitHub API calls (see server/api/getLatestRelease.ts); consider reusing that token here (when set) to raise rate limits and improve reliability, while still keeping caching in place.

Suggested change
try {
const response = await fetch(`https://api.github.com/repos/${repo}`)
const config = useRuntimeConfig()
const githubToken = (config as { githubRepoToken?: string }).githubRepoToken
try {
const headers: Record<string, string> = {
'User-Agent': 'jabref-online',
Accept: 'application/vnd.github.v3+json',
}
if (githubToken) {
headers.Authorization = `token ${githubToken}`
}
const response = await fetch(`https://api.github.com/repos/${repo}`, {
headers,
})

Copilot uses AI. Check for mistakes.
if (!response.ok) {
throw new Error(`GitHub API responded with ${response.status}`)
}

const data = (await response.json()) as { stargazers_count?: number }

return {
stars: data.stargazers_count ?? 0,
}
} catch (error) {
console.debug('Failed to fetch GitHub stars for repo', repo, error)
throw createError({
statusCode: 500,
message: 'Failed to fetch GitHub stars',
})
}
})
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions caching, but this endpoint currently fetches GitHub on every request. In production this is likely to hit GitHub’s unauthenticated rate limits and adds latency to every page view. Consider adding server-side caching (e.g., Nitro storage-based TTL cache / cached event handler) and/or setting appropriate cache headers so repeated requests reuse the cached star count for a period (e.g., 5–30 minutes).

Suggested change
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const repo = query.repo as string
if (!repo) {
throw createError({
statusCode: 400,
message: 'Missing repo parameter',
})
}
try {
const response = await fetch(`https://api.github.com/repos/${repo}`)
if (!response.ok) {
throw new Error(`GitHub API responded with ${response.status}`)
}
const data = (await response.json()) as { stargazers_count?: number }
return {
stars: data.stargazers_count ?? 0,
}
} catch (error) {
console.debug('Failed to fetch GitHub stars for repo', repo, error)
throw createError({
statusCode: 500,
message: 'Failed to fetch GitHub stars',
})
}
})
export default defineCachedEventHandler(
async (event) => {
const query = getQuery(event)
const repo = query.repo as string
if (!repo) {
throw createError({
statusCode: 400,
message: 'Missing repo parameter',
})
}
// Allow shared caches (CDN/reverse proxy) to reuse this response.
// This is aligned with the server-side TTL below.
event.node.res.setHeader(
'Cache-Control',
'public, s-maxage=300, stale-while-revalidate=600',
)
try {
const response = await fetch(`https://api.github.com/repos/${repo}`)
if (!response.ok) {
throw new Error(`GitHub API responded with ${response.status}`)
}
const data = (await response.json()) as { stargazers_count?: number }
return {
stars: data.stargazers_count ?? 0,
}
} catch (error) {
console.debug('Failed to fetch GitHub stars for repo', repo, error)
throw createError({
statusCode: 500,
message: 'Failed to fetch GitHub stars',
})
}
},
{
// Cache the computed star count on the server for 5 minutes
maxAge: 300,
},
)

Copilot uses AI. Check for mistakes.
Loading