kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: fix github auth flow
rodzic
f9254b5965
commit
ebd3e0928a
|
@ -0,0 +1,28 @@
|
||||||
|
import type { DefaultHonoEnv } from '@agentic/platform-hono'
|
||||||
|
import type { OpenAPIHono } from '@hono/zod-openapi'
|
||||||
|
import { assert } from '@agentic/platform-core'
|
||||||
|
|
||||||
|
import { authStorage } from './utils'
|
||||||
|
|
||||||
|
export function registerV1AuthGitHubOAuthCallback(
|
||||||
|
app: OpenAPIHono<DefaultHonoEnv>
|
||||||
|
) {
|
||||||
|
return app.get('auth/github/callback', async (c) => {
|
||||||
|
const logger = c.get('logger')
|
||||||
|
const query = c.req.query()
|
||||||
|
|
||||||
|
assert(query.state, 400, 'State is required')
|
||||||
|
|
||||||
|
const entry = await authStorage.get(['github', query.state, 'redirectUri'])
|
||||||
|
assert(entry, 400, 'Redirect URI not found')
|
||||||
|
const redirectUri = entry.redirectUri
|
||||||
|
assert(entry.redirectUri, 400, 'Redirect URI not found')
|
||||||
|
|
||||||
|
const url = new URL(
|
||||||
|
`${redirectUri}?${new URLSearchParams(query).toString()}`
|
||||||
|
).toString()
|
||||||
|
logger.info('GitHub auth callback', query, '=>', url)
|
||||||
|
|
||||||
|
return c.redirect(url)
|
||||||
|
})
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ const route = createRoute({
|
||||||
tags: ['auth'],
|
tags: ['auth'],
|
||||||
operationId: 'exchangeOAuthCodeWithGitHub',
|
operationId: 'exchangeOAuthCodeWithGitHub',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: 'auth/github',
|
path: 'auth/github/exchange',
|
||||||
security: openapiAuthenticatedSecuritySchemas,
|
security: openapiAuthenticatedSecuritySchemas,
|
||||||
request: {
|
request: {
|
||||||
body: {
|
body: {
|
||||||
|
@ -51,7 +51,7 @@ const route = createRoute({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export function registerV1AuthExchangeOAuthCodeWithGitHub(
|
export function registerV1AuthGitHubOAuthExchange(
|
||||||
app: OpenAPIHono<DefaultHonoEnv>
|
app: OpenAPIHono<DefaultHonoEnv>
|
||||||
) {
|
) {
|
||||||
return app.openapi(route, async (c) => {
|
return app.openapi(route, async (c) => {
|
|
@ -0,0 +1,71 @@
|
||||||
|
import type { DefaultHonoEnv } from '@agentic/platform-hono'
|
||||||
|
import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
import { env } from '@/lib/env'
|
||||||
|
import {
|
||||||
|
openapiAuthenticatedSecuritySchemas,
|
||||||
|
openapiErrorResponse404,
|
||||||
|
openapiErrorResponses
|
||||||
|
} from '@/lib/openapi-utils'
|
||||||
|
|
||||||
|
import { authStorage } from './utils'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
description: 'Starts a GitHub OAuth flow.',
|
||||||
|
tags: ['auth'],
|
||||||
|
operationId: 'initGitHubOAuthFlow',
|
||||||
|
method: 'get',
|
||||||
|
path: 'auth/github/init',
|
||||||
|
security: openapiAuthenticatedSecuritySchemas,
|
||||||
|
request: {
|
||||||
|
query: z
|
||||||
|
.object({
|
||||||
|
redirect_uri: z.string(),
|
||||||
|
client_id: z.string().optional(),
|
||||||
|
scope: z.string().optional()
|
||||||
|
})
|
||||||
|
.passthrough()
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
302: {
|
||||||
|
description: 'Redirected to GitHub'
|
||||||
|
},
|
||||||
|
...openapiErrorResponses,
|
||||||
|
...openapiErrorResponse404
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerV1AuthGitHubOAuthInitFlow(
|
||||||
|
app: OpenAPIHono<DefaultHonoEnv>
|
||||||
|
) {
|
||||||
|
return app.openapi(route, async (c) => {
|
||||||
|
const logger = c.get('logger')
|
||||||
|
const {
|
||||||
|
client_id: clientId = env.GITHUB_CLIENT_ID,
|
||||||
|
scope = 'user:email',
|
||||||
|
redirect_uri: redirectUri
|
||||||
|
} = c.req.query()
|
||||||
|
|
||||||
|
const state = crypto.randomUUID()
|
||||||
|
|
||||||
|
// TODO: unique identifier
|
||||||
|
await authStorage.set(['github', state, 'redirectUri'], { redirectUri })
|
||||||
|
|
||||||
|
const publicRedirectUri = `${env.apiBaseUrl}/v1/auth/github/callback`
|
||||||
|
|
||||||
|
const url = new URL('https://github.com/login/oauth/authorize')
|
||||||
|
url.searchParams.append('client_id', clientId)
|
||||||
|
url.searchParams.append('scope', scope)
|
||||||
|
url.searchParams.append('state', state)
|
||||||
|
url.searchParams.append('redirect_uri', publicRedirectUri)
|
||||||
|
|
||||||
|
logger.info('Redirecting to GitHub', {
|
||||||
|
url: url.toString(),
|
||||||
|
clientId,
|
||||||
|
scope,
|
||||||
|
publicRedirectUri
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.redirect(url.toString())
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { DrizzleAuthStorage } from '@/lib/drizzle-auth-storage'
|
||||||
|
|
||||||
|
export const authStorage = DrizzleAuthStorage()
|
|
@ -6,7 +6,9 @@ import type { AuthenticatedHonoEnv } from '@/lib/types'
|
||||||
import * as middleware from '@/lib/middleware'
|
import * as middleware from '@/lib/middleware'
|
||||||
import { registerOpenAPIErrorResponses } from '@/lib/openapi-utils'
|
import { registerOpenAPIErrorResponses } from '@/lib/openapi-utils'
|
||||||
|
|
||||||
import { registerV1AuthExchangeOAuthCodeWithGitHub } from './auth/github'
|
import { registerV1AuthGitHubOAuthCallback } from './auth/github-callback'
|
||||||
|
import { registerV1AuthGitHubOAuthExchange } from './auth/github-exchange'
|
||||||
|
import { registerV1AuthGitHubOAuthInitFlow } from './auth/github-init'
|
||||||
import { registerV1AuthSignInWithPassword } from './auth/sign-in-with-password'
|
import { registerV1AuthSignInWithPassword } from './auth/sign-in-with-password'
|
||||||
import { registerV1AuthSignUpWithPassword } from './auth/sign-up-with-password'
|
import { registerV1AuthSignUpWithPassword } from './auth/sign-up-with-password'
|
||||||
import { registerV1AdminConsumersActivateConsumer } from './consumers/admin-activate-consumer'
|
import { registerV1AdminConsumersActivateConsumer } from './consumers/admin-activate-consumer'
|
||||||
|
@ -78,7 +80,9 @@ registerHealthCheck(publicRouter)
|
||||||
// Auth
|
// Auth
|
||||||
registerV1AuthSignInWithPassword(publicRouter)
|
registerV1AuthSignInWithPassword(publicRouter)
|
||||||
registerV1AuthSignUpWithPassword(publicRouter)
|
registerV1AuthSignUpWithPassword(publicRouter)
|
||||||
registerV1AuthExchangeOAuthCodeWithGitHub(publicRouter)
|
registerV1AuthGitHubOAuthExchange(publicRouter)
|
||||||
|
registerV1AuthGitHubOAuthInitFlow(publicRouter)
|
||||||
|
registerV1AuthGitHubOAuthCallback(publicRouter)
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
registerV1UsersGetUser(privateRouter)
|
registerV1UsersGetUser(privateRouter)
|
||||||
|
|
|
@ -42,11 +42,15 @@ export function parseEnv(inputEnv: Record<string, unknown>) {
|
||||||
)
|
)
|
||||||
|
|
||||||
const isStripeLive = env.STRIPE_SECRET_KEY.startsWith('sk_live_')
|
const isStripeLive = env.STRIPE_SECRET_KEY.startsWith('sk_live_')
|
||||||
|
const apiBaseUrl = baseEnv.isProd
|
||||||
|
? 'https://api.agentic.so'
|
||||||
|
: 'http://localhost:3001'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...baseEnv,
|
...baseEnv,
|
||||||
...env,
|
...env,
|
||||||
isStripeLive
|
isStripeLive,
|
||||||
|
apiBaseUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { env } from '@/lib/env'
|
||||||
import * as middleware from '@/lib/middleware'
|
import * as middleware from '@/lib/middleware'
|
||||||
|
|
||||||
import { initExitHooks } from './lib/exit-hooks'
|
import { initExitHooks } from './lib/exit-hooks'
|
||||||
import { registerOAuthRedirect } from './oauth-redirect'
|
// import { registerOAuthRedirect } from './oauth-redirect'
|
||||||
|
|
||||||
export const app = new OpenAPIHono<DefaultHonoEnv>()
|
export const app = new OpenAPIHono<DefaultHonoEnv>()
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ app.use(middleware.accessLogger)
|
||||||
app.use(middleware.responseTime)
|
app.use(middleware.responseTime)
|
||||||
|
|
||||||
// TODO: top-level auth routes
|
// TODO: top-level auth routes
|
||||||
registerOAuthRedirect(app)
|
// registerOAuthRedirect(app)
|
||||||
|
|
||||||
// Mount all v1 API routes
|
// Mount all v1 API routes
|
||||||
app.route('/v1', apiV1)
|
app.route('/v1', apiV1)
|
||||||
|
|
|
@ -212,12 +212,10 @@ export class AgenticApiClient {
|
||||||
scope?: string
|
scope?: string
|
||||||
clientId?: string
|
clientId?: string
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
const publicRedirectUri = `${this.apiBaseUrl}/oauth/callback?${new URLSearchParams({ uri: redirectUri }).toString()}`
|
const url = new URL(`${this.apiBaseUrl}/v1/auth/github/init`)
|
||||||
|
|
||||||
const url = new URL('https://github.com/login/oauth/authorize')
|
|
||||||
url.searchParams.append('client_id', clientId)
|
url.searchParams.append('client_id', clientId)
|
||||||
url.searchParams.append('scope', scope)
|
url.searchParams.append('scope', scope)
|
||||||
url.searchParams.append('redirect_uri', publicRedirectUri)
|
url.searchParams.append('redirect_uri', redirectUri)
|
||||||
|
|
||||||
return url.toString()
|
return url.toString()
|
||||||
}
|
}
|
||||||
|
@ -226,7 +224,9 @@ export class AgenticApiClient {
|
||||||
async exchangeOAuthCodeWithGitHub(
|
async exchangeOAuthCodeWithGitHub(
|
||||||
json: OperationBody<'exchangeOAuthCodeWithGitHub'>
|
json: OperationBody<'exchangeOAuthCodeWithGitHub'>
|
||||||
): Promise<AuthSession> {
|
): Promise<AuthSession> {
|
||||||
this._authSession = await this.ky.post('v1/auth/github', { json }).json()
|
this._authSession = await this.ky
|
||||||
|
.post('v1/auth/github/exchange', { json })
|
||||||
|
.json()
|
||||||
|
|
||||||
this.onUpdateAuth?.(this._authSession)
|
this.onUpdateAuth?.(this._authSession)
|
||||||
return this._authSession
|
return this._authSession
|
||||||
|
|
|
@ -55,7 +55,7 @@ export interface paths {
|
||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
"/v1/auth/github": {
|
"/v1/auth/github/exchange": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: never;
|
header?: never;
|
||||||
|
@ -64,7 +64,7 @@ export interface paths {
|
||||||
};
|
};
|
||||||
get?: never;
|
get?: never;
|
||||||
put?: never;
|
put?: never;
|
||||||
/** @description Exchanges GitHub code for auth session. */
|
/** @description Exchanges a GitHub OAuth code for an Agentic auth session. */
|
||||||
post: operations["exchangeOAuthCodeWithGitHub"];
|
post: operations["exchangeOAuthCodeWithGitHub"];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
options?: never;
|
options?: never;
|
||||||
|
@ -72,6 +72,23 @@ export interface paths {
|
||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/v1/auth/github/init": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
/** @description Starts a GitHub OAuth flow. */
|
||||||
|
get: operations["initGitHubOAuthFlow"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/v1/users/{userId}": {
|
"/v1/users/{userId}": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
@ -1021,6 +1038,32 @@ export interface operations {
|
||||||
404: components["responses"]["404"];
|
404: components["responses"]["404"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
initGitHubOAuthFlow: {
|
||||||
|
parameters: {
|
||||||
|
query: {
|
||||||
|
redirect_uri: string;
|
||||||
|
client_id?: string;
|
||||||
|
scope?: string;
|
||||||
|
};
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Redirected to GitHub */
|
||||||
|
302: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
400: components["responses"]["400"];
|
||||||
|
401: components["responses"]["401"];
|
||||||
|
403: components["responses"]["403"];
|
||||||
|
404: components["responses"]["404"];
|
||||||
|
};
|
||||||
|
};
|
||||||
getUser: {
|
getUser: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|
Ładowanie…
Reference in New Issue