From d5708f506b9e16446e77be867c18ef3a25534353 Mon Sep 17 00:00:00 2001 From: anoopkarnik Date: Thu, 18 Jul 2024 11:39:46 +0530 Subject: [PATCH] Added all Youtube OAuth --- .../connections/youtube-connections.tsx | 22 ++++++++ .../app/(dashboard)/connections/page.tsx | 22 ++++++-- .../app/api/callback/notion/route.ts | 5 +- .../app/api/callback/openai/route.ts | 3 +- .../app/api/callback/youtube/route.ts | 33 ++++++------ .../app/api/oauth/youtube/route.ts | 30 +++++++++++ .../components/ConnectionClient.tsx | 4 +- .../migration.sql | 25 +++++++++ .../migration.sql | 10 ++++ .../prisma-db/prisma/schema/connection.prisma | 14 +++++ packages/prisma-db/prisma/schema/user.prisma | 27 +++++----- packages/prisma-db/src/youtube.ts | 51 +++++++++++++++++++ 12 files changed, 207 insertions(+), 39 deletions(-) create mode 100644 apps/dashboard-app/actions/connections/youtube-connections.tsx create mode 100644 apps/dashboard-app/app/api/oauth/youtube/route.ts create mode 100644 packages/prisma-db/prisma/migrations/20240718031435_added_youtube_oauth/migration.sql create mode 100644 packages/prisma-db/prisma/migrations/20240718034932_added_youtube_oauth/migration.sql create mode 100644 packages/prisma-db/src/youtube.ts diff --git a/apps/dashboard-app/actions/connections/youtube-connections.tsx b/apps/dashboard-app/actions/connections/youtube-connections.tsx new file mode 100644 index 0000000..cf58451 --- /dev/null +++ b/apps/dashboard-app/actions/connections/youtube-connections.tsx @@ -0,0 +1,22 @@ +'use server' + +import { createYoutube, getYoutubeByAccessToken, getYoutubeByUserId } from '@repo/prisma-db/repo/youtube' +import { get } from 'http' + + +export const onYoutubeConnection = async ({access_token, refresh_token, scopes, userId}: any) => { + if(access_token){ + console.log('access_token in actions', access_token) + const connected = await getYoutubeByAccessToken(access_token) + console.log('connected', connected) + if (!connected){ + const youtube = await createYoutube({access_token, refresh_token, scopes, userId}) + } + } + +} + +export const getYoutubeConnection = async (userId: string) => { + const connection = await getYoutubeByUserId(userId) + return connection +} \ No newline at end of file diff --git a/apps/dashboard-app/app/(dashboard)/connections/page.tsx b/apps/dashboard-app/app/(dashboard)/connections/page.tsx index 65b2e28..546cdca 100644 --- a/apps/dashboard-app/app/(dashboard)/connections/page.tsx +++ b/apps/dashboard-app/app/(dashboard)/connections/page.tsx @@ -8,6 +8,7 @@ import { useSearchParams } from 'next/navigation' import ConnectionClient from '../../../components/ConnectionClient' import { onOpenAIConnection } from '../../../actions/connections/openai-connections' import { ConnectionsContext } from '../../../providers/connections-provider' +import { onYoutubeConnection } from '../../../actions/connections/youtube-connections' type Props = { searchParams?: { [key: string]: string | undefined } @@ -17,11 +18,14 @@ const Connections = () => { const params = useSearchParams(); const access_token = params.get('access_token') + const refresh_token = params.get('refresh_token') + const scopes = params.get('scopes') const workspace_name = params.get('workspace_name') const workspace_icon = params.get('workspace_icon') const workspace_id = params.get('workspace_id') const database_id = params.get('database_id') const apiKey = params.get('apiKey') + const type = params.get('type') const session = useSession() const user = session?.data?.user const userId = user?.id @@ -31,10 +35,18 @@ const Connections = () => { useEffect(() =>{ const onUserConnection = async () =>{ - // @ts-ignore - await onNotionConnection({access_token,workspace_id,workspace_icon,workspace_name,database_id,userId}) - // @ts-ignore - await onOpenAIConnection({apiKey,userId}) + if (type === 'Notion'){ + // @ts-ignore + await onNotionConnection({access_token,workspace_id,workspace_icon,workspace_name,database_id,userId}) + } + if (type === 'OpenAI'){ + // @ts-ignore + await onOpenAIConnection({apiKey,userId}) + } + if (type === 'Youtube'){ + // @ts-ignore + await onYoutubeConnection({access_token,refresh_token,scopes,userId}) + } const user_info = await getUserInfo(userId || '') const newConnections: Record = {} user_info?.connections.forEach((connection: any) => { @@ -43,7 +55,7 @@ const Connections = () => { setConnections(newConnections) } onUserConnection() - },[access_token,workspace_id,workspace_icon,workspace_name,database_id,apiKey,userId]) + },[access_token,refresh_token, scopes, workspace_id,workspace_icon,workspace_name,database_id,apiKey,userId,type]) return (
diff --git a/apps/dashboard-app/app/api/callback/notion/route.ts b/apps/dashboard-app/app/api/callback/notion/route.ts index a8d6af9..adfc1a9 100644 --- a/apps/dashboard-app/app/api/callback/notion/route.ts +++ b/apps/dashboard-app/app/api/callback/notion/route.ts @@ -40,13 +40,14 @@ export async function GET(req: NextRequest) { : ''; console.log(`Number of databases connected: ${databasesPages?.results?.length}`); + const type = 'Notion'; return NextResponse.redirect( - `${process.env.NEXT_PUBLIC_URL}/dashboard/connections?access_token=${response.data.access_token}&workspace_name=${response.data.workspace_name}&workspace_icon=${response.data.workspace_icon}&workspace_id=${response.data.workspace_id}&database_id=${databaseId}` + `${process.env.NEXT_PUBLIC_URL}/connections?access_token=${response.data.access_token}&workspace_name=${response.data.workspace_name}&workspace_icon=${response.data.workspace_icon}&workspace_id=${response.data.workspace_id}&database_id=${databaseId}&type=${type}` ); } } - return NextResponse.redirect(`${process.env.NEXT_PUBLIC_URL}/dashboard/connections`); + return NextResponse.redirect(`${process.env.NEXT_PUBLIC_URL}/connections`); } diff --git a/apps/dashboard-app/app/api/callback/openai/route.ts b/apps/dashboard-app/app/api/callback/openai/route.ts index 466c200..26d80ac 100644 --- a/apps/dashboard-app/app/api/callback/openai/route.ts +++ b/apps/dashboard-app/app/api/callback/openai/route.ts @@ -2,8 +2,9 @@ import { NextRequest, NextResponse } from 'next/server'; export async function GET(req: NextRequest) { const apiKey = req.nextUrl.searchParams.get('apiKey'); + const type = 'OpenAI'; if (apiKey) { - return NextResponse.redirect(`${process.env.NEXT_PUBLIC_URL}/dashboard/connections?apiKey=${apiKey}`); + return NextResponse.redirect(`${process.env.NEXT_PUBLIC_URL}/dashboard/connections?apiKey=${apiKey}&type=${type}`); } return NextResponse.redirect(`${process.env.NEXT_PUBLIC_URL}/dashboard/connections`); } diff --git a/apps/dashboard-app/app/api/callback/youtube/route.ts b/apps/dashboard-app/app/api/callback/youtube/route.ts index 3aa405f..e713dca 100644 --- a/apps/dashboard-app/app/api/callback/youtube/route.ts +++ b/apps/dashboard-app/app/api/callback/youtube/route.ts @@ -2,22 +2,23 @@ import axios from 'axios'; import { NextRequest, NextResponse } from 'next/server'; import { google } from 'googleapis'; -const oauth2Client = new google.auth.OAuth2( - process.env.NEXT_PUBLIC_YOUTUBE_CLIENT_ID, - process.env.NEXT_PUBLIC_YOUTUBE_CLIENT_SECRET, - process.env.NEXT_PUBLIC_YOUTUBE_REDIRECT_URI -); +export async function GET(req: NextRequest) { -// generate a url that asks permissions for Blogger and Google Calendar scopes -const scopes = [ - 'https://www.googleapis.com/auth/blogger', - 'https://www.googleapis.com/auth/calendar' -]; + const oauth2Client = new google.auth.OAuth2( + process.env.NEXT_PUBLIC_YOUTUBE_CLIENT_ID, + process.env.NEXT_PUBLIC_YOUTUBE_CLIENT_SECRET, + process.env.NEXT_PUBLIC_YOUTUBE_REDIRECT_URI + ); + + const code = req.nextUrl.searchParams.get('code'); + const response = await oauth2Client.getToken(code as string); + const type = 'Youtube'; + if (response){ + return NextResponse.redirect( + `${process.env.NEXT_PUBLIC_URL}/connections?access_token=${response.tokens.access_token}&refresh_token=${response.tokens.refresh_token}&scopes=${response.tokens.scope}&type=${type}` + ); + } -const url = oauth2Client.generateAuthUrl({ - // 'online' (default) or 'offline' (gets refresh_token) - access_type: 'offline', - // If you only need one scope you can pass it as a string - scope: scopes -}); \ No newline at end of file + return NextResponse.redirect(`${process.env.NEXT_PUBLIC_URL}/connections`); +} diff --git a/apps/dashboard-app/app/api/oauth/youtube/route.ts b/apps/dashboard-app/app/api/oauth/youtube/route.ts new file mode 100644 index 0000000..3ab4b05 --- /dev/null +++ b/apps/dashboard-app/app/api/oauth/youtube/route.ts @@ -0,0 +1,30 @@ +import axios from 'axios'; +import { NextRequest, NextResponse } from 'next/server'; +import { google } from 'googleapis'; + + +export async function GET(req: NextRequest) { + + const oauth2Client = new google.auth.OAuth2( + process.env.NEXT_PUBLIC_YOUTUBE_CLIENT_ID, + process.env.NEXT_PUBLIC_YOUTUBE_CLIENT_SECRET, + process.env.NEXT_PUBLIC_YOUTUBE_REDIRECT_URI + ); + + // generate a url that asks permissions for Blogger and Google Calendar scopes + const scopes = [ + 'https://www.googleapis.com/auth/youtube', + 'https://www.googleapis.com/auth/youtube.force-ssl', + 'https://www.googleapis.com/auth/youtube.readonly', + 'https://www.googleapis.com/auth/youtube.upload', + ]; + + const url = oauth2Client.generateAuthUrl({ + // 'online' (default) or 'offline' (gets refresh_token) + access_type: 'offline', + + // If you only need one scope you can pass it as a string + scope: scopes + }); + return NextResponse.redirect(url); +} \ No newline at end of file diff --git a/apps/dashboard-app/components/ConnectionClient.tsx b/apps/dashboard-app/components/ConnectionClient.tsx index c47817e..4addbe4 100644 --- a/apps/dashboard-app/components/ConnectionClient.tsx +++ b/apps/dashboard-app/components/ConnectionClient.tsx @@ -37,8 +37,8 @@ const ConnectionClient = ({description,type,icon,title,connected,formElements,pu setOauthUrl(process.env.NEXT_PUBLIC_NOTION_OAUTH_URL as string) } else if (type === 'Youtube'){ - setCallbackUrl('') - setOauthUrl(process.env.NEXT_PUBLIC_YOUTUBE_REDIRECT_URI as string) + setCallbackUrl(process.env.NEXT_PUBLIC_URL+'/api/callback/youtube') + setOauthUrl(process.env.NEXT_PUBLIC_YOUTUBE_OAUTH_URL as string) } },[type]) diff --git a/packages/prisma-db/prisma/migrations/20240718031435_added_youtube_oauth/migration.sql b/packages/prisma-db/prisma/migrations/20240718031435_added_youtube_oauth/migration.sql new file mode 100644 index 0000000..3d57c29 --- /dev/null +++ b/packages/prisma-db/prisma/migrations/20240718031435_added_youtube_oauth/migration.sql @@ -0,0 +1,25 @@ +-- AlterTable +ALTER TABLE "connection_schema"."Connections" ADD COLUMN "youtubeId" TEXT; + +-- CreateTable +CREATE TABLE "connection_schema"."Youtube" ( + "id" TEXT NOT NULL, + "accessToken" TEXT NOT NULL, + "refreshToken" TEXT NOT NULL, + "scope" TEXT NOT NULL, + "userId" TEXT NOT NULL, + + CONSTRAINT "Youtube_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Youtube_accessToken_key" ON "connection_schema"."Youtube"("accessToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "Youtube_refreshToken_key" ON "connection_schema"."Youtube"("refreshToken"); + +-- AddForeignKey +ALTER TABLE "connection_schema"."Youtube" ADD CONSTRAINT "Youtube_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user_schema"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "connection_schema"."Connections" ADD CONSTRAINT "Connections_youtubeId_fkey" FOREIGN KEY ("youtubeId") REFERENCES "connection_schema"."Youtube"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/packages/prisma-db/prisma/migrations/20240718034932_added_youtube_oauth/migration.sql b/packages/prisma-db/prisma/migrations/20240718034932_added_youtube_oauth/migration.sql new file mode 100644 index 0000000..2b31d2a --- /dev/null +++ b/packages/prisma-db/prisma/migrations/20240718034932_added_youtube_oauth/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to drop the column `scope` on the `Youtube` table. All the data in the column will be lost. + - Added the required column `scopes` to the `Youtube` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "connection_schema"."Youtube" DROP COLUMN "scope", +ADD COLUMN "scopes" TEXT NOT NULL; diff --git a/packages/prisma-db/prisma/schema/connection.prisma b/packages/prisma-db/prisma/schema/connection.prisma index 809a5a8..283612e 100644 --- a/packages/prisma-db/prisma/schema/connection.prisma +++ b/packages/prisma-db/prisma/schema/connection.prisma @@ -62,6 +62,18 @@ model OpenAI { @@schema("connection_schema") } +model Youtube { + id String @id @default(uuid()) + accessToken String? @unique + refreshToken String? @unique + scopes String? + connections Connections[] + User User @relation(fields: [userId], references: [id]) + userId String + + @@schema("connection_schema") +} + model Connections { id String @id @default(uuid()) type String @unique @@ -69,6 +81,8 @@ model Connections { notionId String? OpenAI OpenAI? @relation(fields: [openaiId], references: [id]) openaiId String? + Youtube Youtube? @relation(fields: [youtubeId], references: [id]) + youtubeId String? User User? @relation(fields: [userId], references: [id]) userId String? diff --git a/packages/prisma-db/prisma/schema/user.prisma b/packages/prisma-db/prisma/schema/user.prisma index 4c916c7..e8ac9fb 100644 --- a/packages/prisma-db/prisma/schema/user.prisma +++ b/packages/prisma-db/prisma/schema/user.prisma @@ -8,11 +8,12 @@ model User { accounts Account[] sessions Session[] password String? - Notion Notion[] - OpenAI OpenAI[] - connections Connections[] - workflows Workflows[] - nodes Node[] + Notion Notion[] + OpenAI OpenAI[] + Youtube Youtube[] + connections Connections[] + workflows Workflows[] + nodes Node[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -33,8 +34,8 @@ model Account { id_token String? session_state String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@ -48,8 +49,8 @@ model Session { expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@schema("user_schema") } @@ -57,8 +58,8 @@ model Session { model VerificationToken { id String @id @default(cuid()) email String - token String @unique - expires DateTime + token String @unique + expires DateTime @@unique([email,token]) @@schema("user_schema") @@ -67,8 +68,8 @@ model VerificationToken { model ResetPasswordToken { id String @id @default(cuid()) email String - token String @unique - expires DateTime + token String @unique + expires DateTime @@unique([email,token]) @@schema("user_schema") diff --git a/packages/prisma-db/src/youtube.ts b/packages/prisma-db/src/youtube.ts new file mode 100644 index 0000000..06fd1d9 --- /dev/null +++ b/packages/prisma-db/src/youtube.ts @@ -0,0 +1,51 @@ +import db from './index' + +export const createYoutube = async ({access_token,refresh_token,scopes,userId}:any) =>{ + const youtube = await db.youtube.create({ + data:{ + userId: userId, + accessToken: access_token, + refreshToken: refresh_token, + scopes: scopes, + connections: { + create: { + userId: userId, + type: "Youtube" + } + } + } + }) + return youtube +} + +export const getYoutubeByAccessToken = async (access_token: string) => { + if(access_token){ + try { + const connected = await db.youtube.findUnique({ + where:{ + accessToken: access_token, + }, + include:{ + connections: { + select: { + type: true + } + } + } + }) + return connected; + } catch (error) { + return null + } + } + return null +} + +export const getYoutubeByUserId = async (userId: string) => { + const connection = await db.youtube.findFirst({ + where:{ + userId + } + }) + return connection +} \ No newline at end of file