Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP neon oauth #1578

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use client';
import { useAccount } from '@/components/providers/account-provider';
import { Button } from '@/components/ui/button';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';

export default function NeonForm() {
const { account } = useAccount();
const router = useRouter();
const session = useSession();

async function onSubmit() {
if (!account) {
return;
}
try {
const res = await CreateNeonIntegration(account.id);
if (res.redirect) {
// open up in a new window that is smaller than the current winow and ideally close when complete
router.replace(res.redirect);
return;
}

console.log('done');
} catch (err) {
console.error('Error in form submission:', err);
}
}
// the UI
// projects -> branches -> databases -> role -> host
/*
1. get all projects and list all projects
2. user selects a project
3. get all branches for that project, list all branches
4. user selects a branch
5. get all databases, user selects a database
6. get all roles, list all roles
7. user selects a role
8. get role password from endpoint, get host from endpoint, connect to database, run basic query to validate connections
9. user clicks submit

*/

return (
<div>
<Button onClick={onSubmit}>Connect your Neon account</Button>
</div>
);
}

async function CreateNeonIntegration(accountId: string) {
const res = await fetch(`/api/accounts/${accountId}/connections/neon`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});

if (!res.ok) {
const body = await res.json();
throw new Error(body.message);
}

const data = await res.json();

return data;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
export const NeonLogo = () => {
return (
<svg
width="48"
height="48"
viewBox="0 0 36 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 6.207C0 4.5608 0.65395 2.98203 1.81799 1.81799C2.98203 0.65395 4.5608 0 6.207 0L29.793 0C31.4392 0 33.018 0.65395 34.182 1.81799C35.346 2.98203 36 4.5608 36 6.207V26.267C36 29.813 31.512 31.352 29.336 28.553L22.531 19.799V30.414C22.531 31.8955 21.9425 33.3163 20.8949 34.3639C19.8473 35.4115 18.4265 36 16.945 36H6.207C4.5608 36 2.98203 35.346 1.81799 34.182C0.65395 33.018 0 31.4392 0 29.793L0 6.207ZM6.207 4.966C5.521 4.966 4.966 5.521 4.966 6.206V29.793C4.966 30.479 5.521 31.035 6.206 31.035H17.131C17.474 31.035 17.565 30.757 17.565 30.414V16.18C17.565 12.633 22.053 11.094 24.23 13.894L31.035 22.647V6.207C31.035 5.521 31.099 4.966 30.414 4.966H6.207Z"
fill="#12FFF7"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 6.207C0 4.5608 0.65395 2.98203 1.81799 1.81799C2.98203 0.65395 4.5608 0 6.207 0L29.793 0C31.4392 0 33.018 0.65395 34.182 1.81799C35.346 2.98203 36 4.5608 36 6.207V26.267C36 29.813 31.512 31.352 29.336 28.553L22.531 19.799V30.414C22.531 31.8955 21.9425 33.3163 20.8949 34.3639C19.8473 35.4115 18.4265 36 16.945 36H6.207C4.5608 36 2.98203 35.346 1.81799 34.182C0.65395 33.018 0 31.4392 0 29.793L0 6.207ZM6.207 4.966C5.521 4.966 4.966 5.521 4.966 6.206V29.793C4.966 30.479 5.521 31.035 6.206 31.035H17.131C17.474 31.035 17.565 30.757 17.565 30.414V16.18C17.565 12.633 22.053 11.094 24.23 13.894L31.035 22.647V6.207C31.035 5.521 31.099 4.966 30.414 4.966H6.207Z"
fill="url(#paint0_linear_3251_1370)"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 6.207C0 4.5608 0.65395 2.98203 1.81799 1.81799C2.98203 0.65395 4.5608 0 6.207 0L29.793 0C31.4392 0 33.018 0.65395 34.182 1.81799C35.346 2.98203 36 4.5608 36 6.207V26.267C36 29.813 31.512 31.352 29.336 28.553L22.531 19.799V30.414C22.531 31.8955 21.9425 33.3163 20.8949 34.3639C19.8473 35.4115 18.4265 36 16.945 36H6.207C4.5608 36 2.98203 35.346 1.81799 34.182C0.65395 33.018 0 31.4392 0 29.793L0 6.207ZM6.207 4.966C5.521 4.966 4.966 5.521 4.966 6.206V29.793C4.966 30.479 5.521 31.035 6.206 31.035H17.131C17.474 31.035 17.565 30.757 17.565 30.414V16.18C17.565 12.633 22.053 11.094 24.23 13.894L31.035 22.647V6.207C31.035 5.521 31.099 4.966 30.414 4.966H6.207Z"
fill="url(#paint1_linear_3251_1370)"
/>
<path
d="M29.7933 0C31.4395 0 33.0183 0.65395 34.1823 1.81799C35.3464 2.98203 36.0003 4.5608 36.0003 6.207V26.267C36.0003 29.813 31.5123 31.352 29.3363 28.553L22.5313 19.799V30.414C22.5313 31.8955 21.9428 33.3163 20.8952 34.3639C19.8476 35.4115 18.4268 36 16.9453 36C17.0267 36 17.1074 35.984 17.1826 35.9528C17.2578 35.9216 17.3261 35.876 17.3837 35.8184C17.4413 35.7608 17.487 35.6925 17.5181 35.6173C17.5493 35.542 17.5653 35.4614 17.5653 35.38V16.18C17.5653 12.633 22.0533 11.094 24.2303 13.894L31.0353 22.647V1.241C31.0353 0.556 30.4793 0 29.7933 0Z"
fill="#B9FFB3"
/>
<defs>
<linearGradient
id="paint0_linear_3251_1370"
x1="36"
y1="36"
x2="4.345"
y2="-1.3837e-06"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#B9FFB3" />
<stop offset="1" stopColor="#B9FFB3" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint1_linear_3251_1370"
x1="36"
y1="36"
x2="14.617"
y2="27.683"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#1A1A1A" stopOpacity="0.9" />
<stop offset="1" stopColor="#1A1A1A" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import OverviewContainer from '@/components/containers/OverviewContainer';
import PageHeader from '@/components/headers/PageHeader';
import NeonForm from './NeonForm';
import { NeonLogo } from './NeonLogo';

export default async function Postgres() {
return (
<OverviewContainer
Header={
<PageHeader
header="Neon"
description="Configure a Neon database as a connection"
leftIcon={<NeonLogo />}
/>
}
containerClassName="px-12 md:px-24 lg:px-32"
>
<NeonForm />
</OverviewContainer>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ const CONNECTIONS: ConnectionMeta[] = [
description:
'Amazon Simple Storage Service (Amazon S3) is an object storage service used to store and retrieve any data.',
},
{
urlSlug: 'neon',
name: 'Neon',
description:
'Neon is a serverless Postgres datgabase that separates storage and copmuyte to offer autoscaling, branching and bottomless storage.',
},
];

export default function NewConnection(): ReactElement {
Expand All @@ -30,7 +36,7 @@ export default function NewConnection(): ReactElement {
Header={
<PageHeader
header="Create a new Connection"
description="Connect a new datasource to use in jobs or other synchronizations."
description="Connect a new datasource to use in sync or generate jobs."
pageHeaderContainerClassName="mx-24"
/>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NextRequest, NextResponse } from 'next/server';
import { Issuer, generators } from 'openid-client';

export async function POST(
req: NextRequest,
res: NextResponse
): Promise<NextResponse> {
const state = 'iojeowihfj289yh923h2983hf9';

const neonIssue = await Issuer.discover(
'https://oauth2.neon.tech/.well-known/openid-configuration'
);

// this needs to be stored in a session somewhere
// const code_verifier = generators.codeVerifier();
const code_verifier = 'thisIsTheCodeVerfiierthisIsTheCodeVerfiiers';
Copy link

@vadim2404 vadim2404 Jul 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verfiiers -> Verifiers

looks like misspelling

const code_challenge = generators.codeChallenge(code_verifier);

console.log('code changelle', code_challenge);

const client = new neonIssue.Client({
client_id: 'neosync',
client_secret: 'xxx',
redirect_uris: ['http://localhost:3000/api/integrations/neon/callback'],
response_types: ['code'],
});

const authUrl = client.authorizationUrl({
scope:
'offline offline_access urn:neoncloud:projects:create urn:neoncloud:projects:read',
code_challenge,
code_challenge_method: 'S256',
state: state,
});

return NextResponse.json({ redirect: authUrl });
}
34 changes: 33 additions & 1 deletion frontend/apps/web/app/api/auth/[...nextauth]/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,39 @@ import { addSeconds, isAfter } from 'date-fns';
import NextAuth, { NextAuthConfig } from 'next-auth';

function getProviders(): NextAuthConfig['providers'] {
const providers: NextAuthConfig['providers'] = [];
const providers: NextAuthConfig['providers'] = [
// {
// id: 'neon',
// name: 'Neon',
// type: 'oauth',
// wellKnown: 'https://oauth2.neon.tech/.well-known/openid-configuration',
// authorization: {
// url: 'https://oauth2.neon.tech/oauth2/auth',
// params: {
// grant_type: ['authorization_code'],
// response_type: 'code',
// scope:
// 'offline offline_access urn:neoncloud:projects:create urn:neoncloud:projects:read',
// redirect_uri: 'http://localhost:3000/api/integrations/neon/callback',
// },
// },
// issuer: 'https://oauth2.neon.tech/',
// token: 'https://oauth2.neon.tech/oauth2/token',
// checks: ['pkce', 'state'],
// client: {
// token_endpoint_auth_method: 'client_secret_post',
// },
// profile(profile) {
// return {
// id: profile.sub,
// name: profile.name,
// email: profile.email,
// };
// },
// clientId: 'neosync',
// clientSecret: 'eseH+4a3Ub3aZcqVv+RY+5BbBkvZTXD6OQGc279l1BE=',
// },
];
const authConfig = getOAuthConfig();
if (authConfig) {
providers.push({
Expand Down
57 changes: 57 additions & 0 deletions frontend/apps/web/app/api/integrations/neon/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import axios from 'axios';
import { NextRequest, NextResponse } from 'next/server';

export async function GET(
req: NextRequest,
res: NextResponse
): Promise<NextResponse> {
const { method } = req;

// Check if the request method is allowed
if (method !== 'GET') {
return NextResponse.json(
{ message: 'Method Not Allowed' },
{ status: 405 }
);
}

const url = new URL(req.url);
const params = new URLSearchParams(url.searchParams);
const code_verifier = 'thisIsTheCodeVerfiierthisIsTheCodeVerfiiers';
console.log('code', params.get('code'));
console.log('params', params);

// // compare this with the original state valu eto ensure that the original request came from our application and not from a third party
//TODO: figure out a way to get the original state value
/* const state = params.get('state');
if(state != params.get('state)){
return NextResponse.json(
{ message: 'State verification codes do not match' },
{ status: 500 }
);
}
*/

const queryParams = new URLSearchParams({
client_id: 'neosync',
redirect_uri: 'http://localhost:3000/api/integrations/neon/callback',
client_secret: 'xxxx',
grant_type: 'authorization_code',
code_verifier,
code: params.get('code') ?? '', // exchange for access token
});

try {
const tokenResponse = await axios.post(
`https://oauth2.neon.tech/oauth2/token`,
queryParams.toString()
);

return NextResponse.json({ token: tokenResponse });
} catch (e) {
if (axios.isAxiosError(e)) {
console.error('there was an error', e.response?.data);
}
}
return NextResponse.json(res);
}
8 changes: 8 additions & 0 deletions frontend/apps/web/components/connections/ConnectionIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { NeonLogo } from '@/app/(mgmt)/[account]/new/connection/neon/NeonLogo';
import { ReactElement } from 'react';
import { IconContext } from 'react-icons';
import { DiMysql, DiPostgresql } from 'react-icons/di';
Expand Down Expand Up @@ -37,6 +38,13 @@ export default function ConnectionIcon(props: Props): ReactElement | null {
</IconContext.Provider>
);
}
case 'neon': {
return (
<IconContext.Provider value={{ style: { width, height } }}>
<NeonLogo />
</IconContext.Provider>
);
}

default:
return null;
Expand Down
4 changes: 4 additions & 0 deletions frontend/apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,20 @@
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-table": "^8.12.0",
"@tanstack/react-virtual": "^3.0.4",
"axios": "^1.6.8",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"cmdk": "^0.2.1",
"cookie": "^0.6.0",
"cron-validate": "^1.4.5",
"date-fns": "^3.2.0",
"iron-session": "^8.0.1",
"monaco-editor": "^0.45.0",
"nanoid": "^5.0.6",
"next": "^14.1.3",
"next-auth": "^5.0.0-beta.4",
"next-themes": "^0.2.1",
"openid-client": "^5.6.5",
"posthog-js": "^1.105.7",
"react": "18.2.0",
"react-day-picker": "^8.10.0",
Expand Down
Loading