Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 10 additions & 67 deletions apps/docs/app/api/code-storage/github/callback/route.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,14 @@
import { type NextRequest, NextResponse } from 'next/server';
import { type NextRequest } from 'next/server';

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const code = searchParams.get('code');
const installationId = searchParams.get('installation_id');
const setupAction = searchParams.get('setup_action');
const state = searchParams.get('state');

// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!code) {
return NextResponse.json({ error: 'No code provided' }, { status: 400 });
}

try {
const tokenResponse = await fetch(
'https://github.com/login/oauth/access_token',
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
client_id: process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID,
client_secret: process.env.GITHUB_CLIENT_SECRET,
code,
}),
}
);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tokenData: any = await tokenResponse.json();
import { CodeStorageSuccessCallback } from './success-callback';

// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (tokenData.error) {
throw new Error(tokenData.error_description ?? tokenData.error);
}
const successCallback = new CodeStorageSuccessCallback({
clientId: process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
platform: 'github',
env: process.env.NODE_ENV === 'production' ? 'production' : 'development',
});

const successUrl = new URL('/code-storage/success', request.url);
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (setupAction) {
successUrl.searchParams.set('setup_action', setupAction);
}
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (installationId) {
successUrl.searchParams.set('installation_id', installationId);
}
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (state) {
successUrl.searchParams.set('state', state);
}
const response = NextResponse.redirect(successUrl);

// Store only the user's OAuth token - it works across all installations
response.cookies.set('github_token', tokenData.access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30,
});

return response;
} catch (error) {
console.error('OAuth error:', error);
return NextResponse.json(
{ error: 'Failed to exchange code for token' },
{ status: 500 }
);
}
export async function GET(request: NextRequest) {
return successCallback.handleRequest(request);
}
156 changes: 156 additions & 0 deletions apps/docs/app/api/code-storage/github/callback/success-callback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { type NextRequest, NextResponse } from 'next/server';

export type GitHubOAuthTokenResponse =
| {
access_token: string;
token_type: 'bearer';
scope: string;
}
| {
error: string;
error_description: string;
error_uri: string;
};

export type AvailablePlatform = 'github';
export type CodeStorageSuccessCallbackOptions = {
clientId: string;
clientSecret: string;
platform: AvailablePlatform;
redirectUrl?: string;
env: 'production' | (string & {});
};

export class CodeStorageSuccessCallback {
private clientId: string;
private clientSecret: string;
private redirectUrl?: string;
private env: 'production' | (string & {}) = 'production';

constructor(options: CodeStorageSuccessCallbackOptions) {
if (options.platform !== 'github') {
throw new Error(
// The error is intentionally outputting an unexpected value
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`CodeStorageSuccessCallback Error: Invalid platform: ${options.platform}`
);
}
this.clientId = options.clientId;
this.clientSecret = options.clientSecret;
this.redirectUrl = options.redirectUrl;
this.env = options.env;
}

private getParams(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;

const code = searchParams.get('code') ?? null;
const installationId = searchParams.get('installation_id') ?? null;
const setupAction = searchParams.get('setup_action') ?? null;
const state = searchParams.get('state') ?? null;

if (code == null || code === '') {
throw new Error('CodeStorageSuccessCallback Error: No code provided');
} else if (installationId == null || installationId === '') {
throw new Error(
'CodeStorageSuccessCallback Error: No installation ID provided'
);
} else if (setupAction == null || setupAction === '') {
throw new Error(
'CodeStorageSuccessCallback Error: No setup action provided'
);
} else if (state == null || state === '') {
throw new Error('CodeStorageSuccessCallback Error: No state provided');
}

return { code, installationId, setupAction, state };
}

private generateRedirectUrl(
request: NextRequest,
{
installationId,
setupAction,
state,
}: { installationId: string; setupAction?: string; state: string }
) {
if (this.redirectUrl != null && this.redirectUrl !== '') {
return this.redirectUrl;
}

const successUrl = new URL('/code-storage/success', request.url);

if (setupAction != null && setupAction !== '') {
successUrl.searchParams.set('setup_action', setupAction);
}
if (installationId != null && installationId !== '') {
successUrl.searchParams.set('installation_id', installationId);
}
if (state != null && state !== '') {
successUrl.searchParams.set('state', state);
}
return successUrl;
}

async handleRequest(request: NextRequest) {
try {
const { code, installationId, setupAction, state } =
this.getParams(request);

const tokenResponse = await fetch(
'https://github.com/login/oauth/access_token',
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
client_id: this.clientId,
client_secret: this.clientSecret,
code,
}),
}
);

const tokenData =
(await tokenResponse.json()) as unknown as GitHubOAuthTokenResponse;

if ('error' in tokenData) {
throw new Error(
tokenData.error_description ??
tokenData.error ??
'error fetching github token'
);
}

const successUrl = this.generateRedirectUrl(request, {
installationId,
setupAction,
state,
});

const response = NextResponse.redirect(successUrl);

response.cookies.set('github_token', tokenData.access_token, {
httpOnly: true,
secure: this.env === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30,
});

return response;
} catch (error) {
console.error('CodeStorageSuccessCallback Error:', error);
return NextResponse.json(
{
error:
error instanceof Error
? error.message
: 'CodeStorageSuccessCallback Error',
},
{ status: 400 }
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export async function GET(request: NextRequest) {
});

if (!response.ok) {
console.error('Failed to fetch installations:', response.statusText);
return NextResponse.json(
{ error: 'Failed to fetch installations' },
{ status: 400 }
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/components/ui/dropdown-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
inset && 'pl-8',
inset === true && 'pl-8',
className
)}
{...props}
Expand Down Expand Up @@ -83,7 +83,7 @@ const DropdownMenuItem = React.forwardRef<
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
inset === true && 'pl-8',
className
)}
{...props}
Expand Down Expand Up @@ -147,7 +147,7 @@ const DropdownMenuLabel = React.forwardRef<
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
inset === true && 'pl-8',
className
)}
{...props}
Expand Down
Loading