mirror of
https://github.com/openclaw/clawhub.git
synced 2026-07-22 03:35:29 -04:00
Merge pull request #1 from ianalloway/devin/1771109978-fix-github-username-sync
fix: sync GitHub profile on login to handle username renames (#303)
This commit is contained in:
+11
-1
@@ -2,6 +2,7 @@ import GitHub from '@auth/core/providers/github'
|
||||
import { convexAuth } from '@convex-dev/auth/server'
|
||||
import type { GenericMutationCtx } from 'convex/server'
|
||||
import { ConvexError } from 'convex/values'
|
||||
import { internal } from './_generated/api'
|
||||
import type { DataModel, Id } from './_generated/dataModel'
|
||||
|
||||
export const BANNED_REAUTH_MESSAGE =
|
||||
@@ -69,14 +70,23 @@ export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
||||
],
|
||||
callbacks: {
|
||||
/**
|
||||
* Block sign-in for deleted/deactivated users.
|
||||
* Block sign-in for deleted/deactivated users and sync GitHub profile.
|
||||
*
|
||||
* Performance note: This callback runs on every OAuth sign-in, but the
|
||||
* audit log query ONLY executes when a legacy deleted user attempts to sign
|
||||
* in (user.deletedAt is set). For active users, this is a single field check.
|
||||
*
|
||||
* The GitHub profile sync is scheduled as a background action to handle
|
||||
* the case where a user renames their GitHub account (fixes #303).
|
||||
*/
|
||||
async afterUserCreatedOrUpdated(ctx, args) {
|
||||
await handleDeletedUserSignIn(ctx, args)
|
||||
|
||||
// Schedule GitHub profile sync to handle username renames (fixes #303)
|
||||
// This runs as a background action so it doesn't block sign-in
|
||||
await ctx.scheduler.runAfter(0, internal.users.syncGitHubProfileAction, {
|
||||
userId: args.userId,
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -7,6 +7,8 @@ const GITHUB_API = 'https://api.github.com'
|
||||
const MIN_ACCOUNT_AGE_MS = 7 * 24 * 60 * 60 * 1000
|
||||
|
||||
type GitHubUser = {
|
||||
login?: string
|
||||
avatar_url?: string
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
@@ -78,3 +80,43 @@ export async function requireGitHubAccountAge(ctx: ActionCtx, userId: Id<'users'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the user's GitHub profile (username, avatar) from the GitHub API.
|
||||
* This handles the case where a user renames their GitHub account.
|
||||
* Uses the immutable GitHub numeric ID to fetch the current profile.
|
||||
*/
|
||||
export async function syncGitHubProfile(ctx: ActionCtx, userId: Id<'users'>) {
|
||||
const user = await ctx.runQuery(internal.users.getByIdInternal, { userId })
|
||||
if (!user || user.deletedAt || user.deactivatedAt) return
|
||||
|
||||
const providerAccountId = await ctx.runQuery(
|
||||
internal.githubIdentity.getGitHubProviderAccountIdInternal,
|
||||
{ userId },
|
||||
)
|
||||
if (!providerAccountId) return
|
||||
|
||||
assertGitHubNumericId(providerAccountId)
|
||||
|
||||
const response = await fetch(`${GITHUB_API}/user/${providerAccountId}`, {
|
||||
headers: buildGitHubHeaders(),
|
||||
})
|
||||
if (!response.ok) {
|
||||
// Silently fail - this is a best-effort sync, not critical path
|
||||
console.warn(`[syncGitHubProfile] GitHub API error for user ${userId}: ${response.status}`)
|
||||
return
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as GitHubUser
|
||||
const newLogin = payload.login?.trim()
|
||||
const newImage = payload.avatar_url?.trim()
|
||||
|
||||
// Only update if the username has changed
|
||||
if (newLogin && newLogin !== user.name) {
|
||||
await ctx.runMutation(internal.users.syncGitHubProfileInternal, {
|
||||
userId,
|
||||
name: newLogin,
|
||||
image: newImage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+52
-2
@@ -2,9 +2,10 @@ import { getAuthUserId } from '@convex-dev/auth/server'
|
||||
import { v } from 'convex/values'
|
||||
import { internal } from './_generated/api'
|
||||
import type { Doc, Id } from './_generated/dataModel'
|
||||
import type { MutationCtx } from './_generated/server'
|
||||
import { internalMutation, internalQuery, mutation, query } from './_generated/server'
|
||||
import type { ActionCtx, MutationCtx } from './_generated/server'
|
||||
import { internalAction, internalMutation, internalQuery, mutation, query } from './_generated/server'
|
||||
import { assertAdmin, assertModerator, requireUser } from './lib/access'
|
||||
import { syncGitHubProfile } from './lib/githubAccount'
|
||||
import { toPublicUser } from './lib/public'
|
||||
import { buildUserSearchResults } from './lib/userSearch'
|
||||
|
||||
@@ -59,6 +60,55 @@ export const setGitHubCreatedAtInternal = internalMutation({
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Sync the user's GitHub profile (username, avatar) when it changes.
|
||||
* This handles the case where a user renames their GitHub account.
|
||||
*/
|
||||
export const syncGitHubProfileInternal = internalMutation({
|
||||
args: {
|
||||
userId: v.id('users'),
|
||||
name: v.string(),
|
||||
image: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await ctx.db.get(args.userId)
|
||||
if (!user || user.deletedAt || user.deactivatedAt) return
|
||||
|
||||
const updates: Record<string, unknown> = {
|
||||
name: args.name,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
|
||||
// Update handle if it was derived from the old username
|
||||
if (user.handle === user.name) {
|
||||
updates.handle = args.name
|
||||
}
|
||||
|
||||
// Update displayName if it was derived from the old username
|
||||
if (user.displayName === user.name || user.displayName === user.handle) {
|
||||
updates.displayName = args.name
|
||||
}
|
||||
|
||||
// Update avatar if provided
|
||||
if (args.image) {
|
||||
updates.image = args.image
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.userId, updates)
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Internal action to sync GitHub profile from the GitHub API.
|
||||
* This is called after login to ensure the username is up-to-date.
|
||||
*/
|
||||
export const syncGitHubProfileAction = internalAction({
|
||||
args: { userId: v.id('users') },
|
||||
handler: async (ctx: ActionCtx, args) => {
|
||||
await syncGitHubProfile(ctx, args.userId)
|
||||
},
|
||||
})
|
||||
|
||||
export const me = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
|
||||
Reference in New Issue
Block a user