From c9dbb5d1dabbd06bcc5a6dd761ce6d75fd8241df Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Sun, 21 Sep 2025 18:54:34 -0400 Subject: [PATCH 1/3] move github accedss to server side --- .../src/components/ui/github-avatar.tsx | 37 +++++++------- .../app/control/src/services/github/client.ts | 2 +- .../app/control/src/services/github/link.ts | 11 ++-- .../app/control/src/services/github/schema.ts | 2 + .../app/control/src/services/github/users.ts | 51 +++++++++++-------- .../app/control/src/trpc/routers/github.ts | 12 +++++ .../app/control/src/trpc/routers/index.ts | 8 +-- 7 files changed, 71 insertions(+), 52 deletions(-) create mode 100644 packages/app/control/src/trpc/routers/github.ts diff --git a/packages/app/control/src/components/ui/github-avatar.tsx b/packages/app/control/src/components/ui/github-avatar.tsx index ab86b9e51..d0d6a2509 100644 --- a/packages/app/control/src/components/ui/github-avatar.tsx +++ b/packages/app/control/src/components/ui/github-avatar.tsx @@ -1,10 +1,11 @@ 'use client'; -import React, { memo, useEffect, useState } from 'react'; -import { Avatar, AvatarFallback, AvatarImage } from './avatar'; import { cn } from '@/lib/utils'; +import type { RouterOutputs } from '@/trpc/client'; +import { api } from '@/trpc/client'; import { SiGithub } from '@icons-pack/react-simple-icons'; -import { searchUser } from '@/services/github/users'; +import React, { memo, useEffect, useState } from 'react'; +import { Avatar, AvatarFallback, AvatarImage } from './avatar'; export const MinimalGithubAvatar = memo(function MinimalGithubAvatar({ login, @@ -32,6 +33,8 @@ export const MinimalGithubAvatar = memo(function MinimalGithubAvatar({ ); }); +type SearchUsersResult = NonNullable; + export const GithubAvatar = memo(function GithubAvatar({ pageUrl, className, @@ -46,7 +49,7 @@ export const GithubAvatar = memo(function GithubAvatar({ linkToProfile?: boolean; }) { const [user, setUser] = useState< - NonNullable>>['items'][number] | null + NonNullable['items'][number] | null >(null); const owner = React.useMemo(() => { @@ -62,32 +65,26 @@ export const GithubAvatar = memo(function GithubAvatar({ } }, [pageUrl]); + const search = api.github.searchUsers.useQuery( + { q: owner ?? '' }, + { enabled: Boolean(owner) } + ); + useEffect(() => { if (!owner) { setUser(null); return; } - let isCurrent = true; - searchUser(owner) - .then(fetched => { - if (!isCurrent) return; - setUser(fetched?.items[0] ?? null); - }) - .catch(() => { - if (!isCurrent) return; - setUser(null); - }); - return () => { - isCurrent = false; - }; - }, [owner]); + const next = search.data?.items?.[0] ?? null; + setUser(next); + }, [owner, search.data]); const avatar = ( {owner ? ( ) : null} @@ -164,7 +161,7 @@ export const GithubAvatar = memo(function GithubAvatar({ ); } - const displayName = user?.name ?? owner; + const displayName = user?.login ?? owner; const profileUrl = pageUrl; return ( diff --git a/packages/app/control/src/services/github/client.ts b/packages/app/control/src/services/github/client.ts index 1fb8fabc0..cc04520f4 100644 --- a/packages/app/control/src/services/github/client.ts +++ b/packages/app/control/src/services/github/client.ts @@ -1,5 +1,5 @@ -import { Octokit } from 'octokit'; import { env } from '@/env'; +import { Octokit } from 'octokit'; export const githubClient = new Octokit({ auth: env.GITHUB_TOKEN, diff --git a/packages/app/control/src/services/github/link.ts b/packages/app/control/src/services/github/link.ts index 01995cdbb..d77141609 100644 --- a/packages/app/control/src/services/github/link.ts +++ b/packages/app/control/src/services/github/link.ts @@ -1,13 +1,8 @@ -import type { z } from 'zod'; - +import { getUser } from '@/services/github/users'; import { getRepo } from './repo'; -import { getUser } from './users'; - -import type { githubLinkSchema } from './schema'; +import type { GithubLink } from './schema'; -export const resolveGithubId = async ( - data: z.infer -) => { +export const resolveGithubId = async (data: GithubLink): Promise => { if (data.type === 'user') { const username = data.url.split('/').pop(); if (!username) { diff --git a/packages/app/control/src/services/github/schema.ts b/packages/app/control/src/services/github/schema.ts index 71a4e4034..2cda1ad1f 100644 --- a/packages/app/control/src/services/github/schema.ts +++ b/packages/app/control/src/services/github/schema.ts @@ -29,3 +29,5 @@ export const githubLinkSchema = z.discriminatedUnion('type', [ ), }), ]); + +export type GithubLink = z.infer; diff --git a/packages/app/control/src/services/github/users.ts b/packages/app/control/src/services/github/users.ts index faa5aade1..6552bcb83 100644 --- a/packages/app/control/src/services/github/users.ts +++ b/packages/app/control/src/services/github/users.ts @@ -1,25 +1,36 @@ +import 'server-only'; import { githubClient } from './client'; -export const getUser = async (username: string) => { - return githubClient.rest.users - .getByUsername({ - username, - }) - .then(res => res.data) - .catch(error => { - console.error('Error getting GitHub user:', error); - return null; - }); +export type SearchUsersResponse = Awaited< + ReturnType +>['data']; +export type SearchedUser = SearchUsersResponse['items'][number]; + +export const searchUsers = async ( + q: string +): Promise => { + try { + const res = await githubClient.rest.search.users({ q }); + return res.data; + } catch (error) { + console.error('Error searching GitHub users:', error); + return null; + } }; -export const searchUser = async (query: string) => { - return githubClient.rest.search - .users({ - q: query, - }) - .then(res => res.data) - .catch(error => { - console.error('Error searching GitHub user:', error); - return null; - }); +export const getUser = async ( + username: string +): Promise => { + try { + const data = await searchUsers(username); + if (!data) return null; + return ( + data.items?.find( + user => user.login?.toLowerCase() === username.toLowerCase() + ) ?? null + ); + } catch (error) { + console.error('Error getting GitHub user:', error); + return null; + } }; diff --git a/packages/app/control/src/trpc/routers/github.ts b/packages/app/control/src/trpc/routers/github.ts new file mode 100644 index 000000000..4a2327347 --- /dev/null +++ b/packages/app/control/src/trpc/routers/github.ts @@ -0,0 +1,12 @@ +import { searchUsers } from '@/services/github/users'; +import z from 'zod'; +import { createTRPCRouter, publicProcedure } from '../trpc'; + +export const githubRouter = createTRPCRouter({ + searchUsers: publicProcedure + .input(z.object({ q: z.string().min(1) })) + .query(async ({ input }) => { + const res = await searchUsers(input.q); + return res; + }), +}); diff --git a/packages/app/control/src/trpc/routers/index.ts b/packages/app/control/src/trpc/routers/index.ts index eeba68192..31919079a 100644 --- a/packages/app/control/src/trpc/routers/index.ts +++ b/packages/app/control/src/trpc/routers/index.ts @@ -1,10 +1,11 @@ import { createCallerFactory, createTRPCRouter } from '../trpc'; -import { appsRouter } from './apps'; -import { userRouter } from './user'; import { adminRouter } from './admin/admin'; -import { uploadRouter } from './upload'; +import { appsRouter } from './apps'; import { creditsRouter } from './credits'; +import { githubRouter } from './github'; +import { uploadRouter } from './upload'; +import { userRouter } from './user'; export const appRouter = createTRPCRouter({ apps: appsRouter, @@ -12,6 +13,7 @@ export const appRouter = createTRPCRouter({ credits: creditsRouter, admin: adminRouter, upload: uploadRouter, + github: githubRouter, }); export type AppRouter = typeof appRouter; From dcb92230c52be400da2969fa375525f8a1895dfd Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Sun, 21 Sep 2025 18:57:37 -0400 Subject: [PATCH 2/3] rm --- packages/app/control/src/services/github/users.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app/control/src/services/github/users.ts b/packages/app/control/src/services/github/users.ts index 6552bcb83..730630570 100644 --- a/packages/app/control/src/services/github/users.ts +++ b/packages/app/control/src/services/github/users.ts @@ -1,4 +1,3 @@ -import 'server-only'; import { githubClient } from './client'; export type SearchUsersResponse = Awaited< From 1f0f554e18e96f71a1b6d05b767185561f527c68 Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Sun, 21 Sep 2025 19:02:43 -0400 Subject: [PATCH 3/3] knip --- packages/app/control/src/services/github/users.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/control/src/services/github/users.ts b/packages/app/control/src/services/github/users.ts index 730630570..8f14a1345 100644 --- a/packages/app/control/src/services/github/users.ts +++ b/packages/app/control/src/services/github/users.ts @@ -1,9 +1,9 @@ import { githubClient } from './client'; -export type SearchUsersResponse = Awaited< +type SearchUsersResponse = Awaited< ReturnType >['data']; -export type SearchedUser = SearchUsersResponse['items'][number]; +type SearchedUser = SearchUsersResponse['items'][number]; export const searchUsers = async ( q: string