Skip to content

Comments

feat: add github connection#1613

Draft
olivermrose wants to merge 4 commits intonpmx-dev:mainfrom
olivermrose:feat/gh-connection
Draft

feat: add github connection#1613
olivermrose wants to merge 4 commits intonpmx-dev:mainfrom
olivermrose:feat/gh-connection

Conversation

@olivermrose
Copy link
Contributor

🔗 Linked issue

#479

🧭 Context

Implements GitHub OAuth, displays whether a repo is starred or not, and the ability to star/unstar through the UI. This does not implement the #32 portion.

Demo
demo.mp4

📚 Description

.env requires GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET from an OAuth app in order to run. I renamed/moved some of the AT Protocol files to distinguish between the auth providers since there's more than one now. I based the GH auth off the existing AT route, and I didn't run into any issues, but it's been a while since I've manually handled oauth flows, so there's a chance I've missed an edge case or something.

For the starring API routes, I just left them all under /github because I wasn't sure if a /star path was needed/wanted.

A couple of questions:

  • Lucide doesn't have filled variants, should I use icons from a different collection?
  • The stargazers link gets replaced by the star button when a user has their gh connected; should this still be accessible? I originally had only the star clickable but I felt that the hit target was too small.
  • The i18n modal keys aren't final, I just created a new group for now under auth.github because I wasn't sure on the process of updating keys (is a find-and-replace enough?), but I will move these to auth.modal

@vercel
Copy link

vercel bot commented Feb 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Feb 23, 2026 8:40pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 23, 2026 8:40pm
npmx-lunaria Ignored Ignored Feb 23, 2026 8:40pm

Request Review

@github-actions
Copy link

github-actions bot commented Feb 23, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
lunaria/files/en-GB.json Localization changed, will be marked as complete.
lunaria/files/en-US.json Source changed, localizations will be marked as outdated.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4497ac5 and 61dd36f.

📒 Files selected for processing (1)
  • server/api/auth/github/index.get.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/api/auth/github/index.get.ts

📝 Walkthrough

Walkthrough

Adds GitHub OAuth and starring support alongside existing Atproto auth: new GitHub composable and GitHub modal UI, AccountMenu updates to show GitHub connection and stacked avatars, server API routes for GitHub OAuth and star management, new shared schema and session github field, Atproto endpoints moved under /api/auth/atproto/, adjustments to useAtproto and modal identifiers, and localisation entries for GitHub-related UI.

Possibly related PRs

Suggested reviewers

  • danielroe
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly relates to the changeset, detailing GitHub OAuth implementation, starring functionality, and file reorganization.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (3)
shared/schemas/github.ts (1)

3-6: Consider adding non-empty string validation for owner and repo.

v.string() accepts empty strings, which would pass validation but fail silently at the GitHub API level. A minLength(1) pipe rejects these at the schema boundary.

♻️ Suggested fix
 export const GitHubStarBodySchema = v.object({
-  owner: v.string(),
-  repo: v.string(),
+  owner: v.pipe(v.string(), v.minLength(1)),
+  repo: v.pipe(v.string(), v.minLength(1)),
 })
server/api/auth/github/session.delete.ts (1)

3-11: Consider returning a structured JSON response for consistency.

Other endpoints in this PR return JSON objects (e.g., { starred: false }). Returning a plain string here makes it harder for clients to handle responses uniformly. A minor inconsistency, but worth aligning.

Suggested fix
-  return 'GitHub disconnected'
+  return { disconnected: true }
server/api/github/star.put.ts (1)

7-47: Significant code duplication with star.delete.ts.

This handler is nearly identical to star.delete.ts — the only differences are the HTTP method (PUT vs DELETE), the Content-Length header, and the returned starred boolean. Consider extracting a shared helper to reduce duplication:

Sketch of shared helper
// server/utils/github-star.ts
export async function toggleGitHubStar(event: H3Event, method: 'PUT' | 'DELETE') {
  const session = await useServerSession(event)
  const github = session.data.github

  if (!github?.accessToken) {
    throw createError({ statusCode: 401, message: 'GitHub account not connected.' })
  }

  const { owner, repo } = v.parse(GitHubStarBodySchema, await readBody(event))

  // ... validation, fetch, error handling ...

  return { starred: method === 'PUT' }
}

Note: the path traversal concern flagged on star.delete.ts applies equally here.


ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fd5a7d and 4497ac5.

📒 Files selected for processing (24)
  • app/components/Header/AccountMenu.client.vue
  • app/components/Header/AtprotoModal.client.vue
  • app/components/Header/GitHubModal.client.vue
  • app/composables/atproto/useAtproto.ts
  • app/composables/github/useGitHub.ts
  • app/composables/github/useGitHubStar.ts
  • app/pages/package/[[org]]/[name].vue
  • i18n/locales/en.json
  • i18n/schema.json
  • lunaria/files/en-GB.json
  • lunaria/files/en-US.json
  • nuxt.config.ts
  • server/api/auth/atproto/index.get.ts
  • server/api/auth/atproto/session.delete.ts
  • server/api/auth/atproto/session.get.ts
  • server/api/auth/github/index.get.ts
  • server/api/auth/github/session.delete.ts
  • server/api/auth/github/session.get.ts
  • server/api/github/star.delete.ts
  • server/api/github/star.put.ts
  • server/api/github/starred.get.ts
  • server/utils/auth.ts
  • shared/schemas/github.ts
  • shared/types/userSession.ts

Comment on lines +19 to +27
const { data: starStatus, refresh } = useFetch<StarStatus>(
() => `/api/github/starred?owner=${owner.value}&repo=${repo.value}`,
{
server: false,
immediate: false,
default: () => ({ starred: false, connected: false }),
watch: false,
},
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Query parameters are not URL-encoded.

owner.value and repo.value are interpolated directly into the URL string. If either value contains characters like &, =, or spaces, the URL will be malformed. Use encodeURIComponent or pass a query object.

Suggested fix using query object
- const { data: starStatus, refresh } = useFetch<StarStatus>(
-   () => `/api/github/starred?owner=${owner.value}&repo=${repo.value}`,
-   {
-     server: false,
-     immediate: false,
-     default: () => ({ starred: false, connected: false }),
-     watch: false,
-   },
- )
+ const { data: starStatus, refresh } = useFetch<StarStatus>(
+   '/api/github/starred',
+   {
+     server: false,
+     immediate: false,
+     default: () => ({ starred: false, connected: false }),
+     watch: false,
+     query: computed(() => ({ owner: owner.value, repo: repo.value })),
+   },
+ )

Comment on lines +893 to +916
<TooltipApp
v-if="isGitHubRepo && isGitHubConnected"
:text="
isStarred ? $t('package.github_star.unstar') : $t('package.github_star.star')
"
position="bottom"
strategy="fixed"
>
<ButtonBase
size="small"
:title="
isStarred ? $t('package.github_star.unstar') : $t('package.github_star.star')
"
:aria-label="
isStarred ? $t('package.github_star.unstar') : $t('package.github_star.star')
"
:aria-pressed="isStarred"
:disabled="isStarActionPending"
:classicon="isStarred ? 'i-lucide:star text-yellow-400' : 'i-lucide:star'"
@click="toggleStar"
>
{{ compactNumberFormatter.format(stars) }}
</ButtonBase>
</TooltipApp>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Star count does not update after toggling.

stars (Line 914) comes from useRepoMeta, which fetches the repository metadata once. After toggleStar, isStarred updates via the optimistic UI in useGitHubStar, but stars remains stale — the count won't increment/decrement. This creates a confusing UX where the icon changes but the number doesn't.

Consider deriving a display count that adjusts based on the toggle delta:

const displayStars = computed(() => {
  if (!isGitHubConnected.value) return stars.value
  // Adjust based on the original fetched state vs current toggled state
  // (requires tracking the initial starred state from the API)
  ...
})

Alternatively, pass the stars value into useGitHubStar so it can return a reactive adjusted count.

Comment on lines +909 to +911
:aria-pressed="isStarred"
:disabled="isStarActionPending"
:classicon="isStarred ? 'i-lucide:star text-yellow-400' : 'i-lucide:star'"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Star states are distinguished only by colour, which is an accessibility concern.

Both starred and unstarred use i-lucide:star; the only visual difference is text-yellow-400. Users who cannot perceive colour differences won't see the state change. The aria-pressed attribute helps assistive technology, but consider adding a visible text label (e.g., "Starred" / "Star") or using distinct icons (the PR description notes Lucide lacks filled variants — a different icon set could help here).

Comment on lines +5 to +9
await serverSession.update({
public: undefined,
oauthSession: undefined,
oauthState: undefined,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd server && git ls-files --exclude-standard | head -20

Repository: npmx-dev/npmx.dev

Length of output: 727


🏁 Script executed:

# Find the file being reviewed and its context
find . -name "session.delete.ts" -o -name "*session*" -type f | grep -E "\.(ts|tsx)$" | head -20

Repository: npmx-dev/npmx.dev

Length of output: 307


🏁 Script executed:

# Search for session type definitions and useSession usage
rg -n "useSession|SessionData|ServerSession" --type ts -A 5 -B 2 | head -100

Repository: npmx-dev/npmx.dev

Length of output: 6867


🏁 Script executed:

# Look at the file being reviewed to understand context
cat -n server/api/auth/atproto/session.delete.ts

Repository: npmx-dev/npmx.dev

Length of output: 565


🏁 Script executed:

# Search for where public, oauthSession, oauthState are used/defined
rg -n "public.*oauth|oauth.*public" --type ts -C 3

Repository: npmx-dev/npmx.dev

Length of output: 576


🏁 Script executed:

# Find session type augmentation or declarations
rg -n "declare.*session|interface.*[Ss]ession|type.*[Ss]ession" --type ts -A 3 | head -80

Repository: npmx-dev/npmx.dev

Length of output: 3806


🏁 Script executed:

cat -n shared/types/userSession.ts

Repository: npmx-dev/npmx.dev

Length of output: 869


🏁 Script executed:

cat -n server/api/auth/github/session.delete.ts

Repository: npmx-dev/npmx.dev

Length of output: 390


🏁 Script executed:

# Check if there's a github field in UserServerSession
rg -A 20 "interface UserServerSession" --type ts

Repository: npmx-dev/npmx.dev

Length of output: 1212


🏁 Script executed:

# Search for where github session data is stored
rg -n "session.*github\|github.*session" --type ts -C 2 | head -60

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check package.json to see h3 version
cat -n package.json | grep -A 5 -B 5 '"h3"'

Repository: npmx-dev/npmx.dev

Length of output: 435


🌐 Web query:

h3 session.update undefined remove key semantics

💡 Result:

In h3 sessions, session.update() is just a thin wrapper around updateSession(event, config, update?). It merges the provided object into session.data using Object.assign only if update is truthy—there is no special “undefined means delete this key” behavior. [1]

What happens with undefined

  • await session.update(undefined) (or no argument) does not change session.data; it just re-seals/writes the session cookie with the current data. [1]
  • await session.update({ foo: undefined }) sets foo to undefined (the property still exists; it’s not removed). [1]

How to remove a key

Use the functional form and delete the property (then return nothing so the Object.assign path is skipped, but your mutation remains on session.data):

await session.update((data) => {
  delete data.foo
})

This works because when update is a function, h3 calls it with session.data, and if it returns undefined, h3 won’t Object.assign, but it will still seal/write the (now mutated) session. [1]

How to remove the whole session

Use await session.clear(). [2]

Sources: h3 useSession/updateSession implementation [1], h3 sessions docs (update/clear usage) [2].


🏁 Script executed:

# Check if there are any tests or documentation about session handling
find . -name "*.md" -o -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "session.update\|useSession" 2>/dev/null | head -5

Repository: npmx-dev/npmx.dev

Length of output: 43


Use the functional form to properly remove ATProto session keys.

The current approach of setting fields to undefined does not delete them from the session in h3. The keys will persist with undefined values, leaving stale session data:

Corrected update pattern
await session.update((data) => {
  delete data.public
  delete data.oauthSession
  delete data.oauthState
})

Alternatively, since this clears only ATProto-specific fields and GitHub is stored separately in the github field, ensure the functional form is used to fully remove the keys rather than just nullifying them.

@serhalp serhalp linked an issue Feb 23, 2026 that may be closed by this pull request
@serhalp serhalp added the social Social features label Feb 23, 2026
@olivermrose
Copy link
Contributor Author

Was attempting to update the star count, but it seems like GitHub's api doesn't immediately update. I can optimistically update the UI, but I'm not sure how to persist the star count on refresh/navigate.

@olivermrose olivermrose marked this pull request as draft February 23, 2026 20:48
@olivermrose
Copy link
Contributor Author

Could also use some guidance for #1613 (comment) since I'm not too familiar with h3 (moreso just want to know the accuracy of this statement)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

social Social features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

github connection

2 participants