The page you are looking for does not exist.
} 8 |10 | 16 | 20 | Start Over 21 | 22 |
23 |163 | {body} 164 |
165 | ) 166 | }) 167 | FormMessage.displayName = "FormMessage" 168 | 169 | export { 170 | useFormField, 171 | Form, 172 | FormItem, 173 | FormLabel, 174 | FormControl, 175 | FormDescription, 176 | FormMessage, 177 | FormField, 178 | } 179 | -------------------------------------------------------------------------------- /app/routes/api/auth/callback.github.ts: -------------------------------------------------------------------------------- 1 | import { createAPIFileRoute } from '@tanstack/start/api' 2 | 3 | import { createSession, github, lucia } from '@/auth' 4 | import { db } from '@/db/client' 5 | import { OAuthAccount, User } from '@/db/schema' 6 | import { OAuth2RequestError } from 'arctic' 7 | import { and, eq } from 'drizzle-orm' 8 | import { parseCookies } from 'vinxi/http' 9 | 10 | interface GitHubUser { 11 | id: number 12 | email: string 13 | name?: string 14 | avatar_url?: string 15 | login: string 16 | verified: boolean 17 | } 18 | 19 | export const Route = createAPIFileRoute('/api/auth/callback/github')({ 20 | GET: async ({ request, params }) => { 21 | console.log('########## GitHub OAuth callback ##########') 22 | const url = new URL(request.url) 23 | const code = url.searchParams.get('code') 24 | const state = url.searchParams.get('state') 25 | const cookies = parseCookies() 26 | const storedState = cookies.github_oauth_state 27 | if (!code || !state || !storedState || state !== storedState) { 28 | console.error( 29 | `Invalid state or code in GitHub OAuth callback: ${JSON.stringify({ code, state, storedState })}`, 30 | ) 31 | console.error(`Cookies: ${JSON.stringify(cookies)}`) 32 | return new Response(null, { 33 | status: 400, 34 | }) 35 | } 36 | 37 | try { 38 | const tokens = await github.validateAuthorizationCode(code) 39 | const userProfile = await fetch('https://api.github.com/user', { 40 | headers: { 41 | Authorization: `Bearer ${tokens.accessToken}`, 42 | }, 43 | }) 44 | const githubUserProfile = (await userProfile.json()) as GitHubUser 45 | // console.log(`GitHub user: ${JSON.stringify(githubUserProfile)}`); 46 | // email can be null if user has made it private. 47 | const existingAccount = await db.query.OAuthAccount.findFirst({ 48 | where: (fields) => 49 | and( 50 | eq(fields.provider, 'github'), 51 | eq(fields.providerAccountId, githubUserProfile.id.toString()), 52 | ), 53 | }) 54 | if (existingAccount) { 55 | const session = await createSession(existingAccount.userId) 56 | const sessionCookie = lucia.createSessionCookie( 57 | session.id, 58 | session.expiresAt, 59 | ) 60 | return new Response(null, { 61 | status: 302, 62 | headers: { 63 | Location: '/', 64 | 'Set-Cookie': sessionCookie.serialize(), 65 | }, 66 | }) 67 | } 68 | if (!githubUserProfile.email) { 69 | const emailResponse = await fetch( 70 | 'https://api.github.com/user/emails', 71 | { 72 | headers: { 73 | Authorization: `Bearer ${tokens.accessToken}`, 74 | }, 75 | }, 76 | ) 77 | const emails = await emailResponse.json() 78 | // [{"email":"email1@test.com","primary":true,"verified":true,"visibility":"public"},{"email":"email2@test.com","primary":false,"verified":true,"visibility":null}] 79 | const primaryEmail = emails.find( 80 | (email: { primary: boolean }) => email.primary, 81 | ) 82 | // TODO verify the email if not verified 83 | if (primaryEmail) { 84 | githubUserProfile.email = primaryEmail.email 85 | githubUserProfile.verified = primaryEmail.verified 86 | } else if (emails.length > 0) { 87 | githubUserProfile.email = emails[0].email 88 | githubUserProfile.verified = emails[0].verified 89 | } 90 | } 91 | // If no existing account check if the a user with the email exists and link the account. 92 | const newUser = await db.transaction(async (tx) => { 93 | const [newUser] = await tx 94 | .insert(User) 95 | .values({ 96 | email: githubUserProfile.email, 97 | name: githubUserProfile.name || githubUserProfile.login, 98 | image: githubUserProfile.avatar_url, 99 | emailVerified: githubUserProfile.verified, 100 | }) 101 | .returning({ 102 | id: User.id, 103 | }) 104 | if (!newUser) { 105 | return null 106 | } 107 | await tx.insert(OAuthAccount).values({ 108 | provider: 'github', 109 | providerAccountId: githubUserProfile.id.toString(), 110 | userId: newUser?.id, 111 | accessToken: tokens.accessToken, 112 | }) 113 | return newUser 114 | }) 115 | if (!newUser) { 116 | console.error('Failed to create user account') 117 | return new Response(null, { 118 | status: 500, 119 | }) 120 | } 121 | const session = await createSession(newUser.id) 122 | const sessionCookie = lucia.createSessionCookie( 123 | session.id, 124 | session.expiresAt, 125 | ) 126 | return new Response(null, { 127 | status: 302, 128 | headers: { 129 | Location: '/', 130 | 'Set-Cookie': sessionCookie.serialize(), 131 | }, 132 | }) 133 | } catch (e) { 134 | console.log(e) 135 | if (e instanceof OAuth2RequestError) { 136 | // bad verification code, invalid credentials, etc 137 | return new Response(null, { 138 | status: 400, 139 | }) 140 | } 141 | return new Response(null, { 142 | status: 500, 143 | }) 144 | } 145 | // TODO: Handle error page 146 | }, 147 | }) 148 | -------------------------------------------------------------------------------- /app/lib/cache.ts: -------------------------------------------------------------------------------- 1 | import { hash } from 'ohash' 2 | import type { Storage, StorageValue } from 'unstorage' 3 | import { createStorage, prefixStorage } from 'unstorage' 4 | 5 | const globalForStorage = globalThis as unknown as { 6 | storage: Storage | undefined 7 | } 8 | export const storage = globalForStorage.storage ?? createStorage() 9 | 10 | export interface CacheEntry{body}
184 |187 | {user.name || user.email} 188 |
189 | {currentUser?.id === user.id && ( 190 | 204 | )} 205 |