From d13391b542c51ed4f0f03c2961398166257160ca Mon Sep 17 00:00:00 2001 From: YukiOnishi <58220747+YukiOnishi1129@users.noreply.github.com> Date: Wed, 4 Sep 2024 13:06:55 +0900 Subject: [PATCH 1/2] set access token --- web/client-v2/src/app/dashboard/page.tsx | 14 +++++++-- .../layout/BaseLayout/LoggedBaseLayout.tsx | 2 +- .../Header/DesktopHeader/DesktopHeader.tsx | 2 +- .../src/components/layout/Header/Header.tsx | 2 +- .../components/provider/ApolloProvider.tsx | 18 +++++++++++- .../Template/ArticleDashboardTemplate.tsx | 2 ++ .../src/features/auth/actions/auth.ts | 12 +++++++- web/client-v2/src/lib/apollo/client.ts | 29 +++++++++++++++---- 8 files changed, 69 insertions(+), 12 deletions(-) diff --git a/web/client-v2/src/app/dashboard/page.tsx b/web/client-v2/src/app/dashboard/page.tsx index 9911a355..df4e84d3 100644 --- a/web/client-v2/src/app/dashboard/page.tsx +++ b/web/client-v2/src/app/dashboard/page.tsx @@ -1,3 +1,13 @@ -export default function Home() { - return
Dashboard
; +import { redirect } from "next/navigation"; + +import { ArticleDashboardTemplate } from "@/features/articles/components/Template"; +import { getUser } from "@/features/auth/actions/user"; + +export default async function Dashboard() { + const user = await getUser(); + if (!user) { + redirect("/login"); + } + + return ; } diff --git a/web/client-v2/src/components/layout/BaseLayout/LoggedBaseLayout.tsx b/web/client-v2/src/components/layout/BaseLayout/LoggedBaseLayout.tsx index 4fd126cb..53935c50 100644 --- a/web/client-v2/src/components/layout/BaseLayout/LoggedBaseLayout.tsx +++ b/web/client-v2/src/components/layout/BaseLayout/LoggedBaseLayout.tsx @@ -14,7 +14,7 @@ type LoggedBaseLayoutProps = { children: ReactNode; }; -export const LoggedBaseLayout: FC = async ({ +export const LoggedBaseLayout: FC = ({ user, children, }) => { diff --git a/web/client-v2/src/components/layout/Header/DesktopHeader/DesktopHeader.tsx b/web/client-v2/src/components/layout/Header/DesktopHeader/DesktopHeader.tsx index 28803eff..2d25ba27 100644 --- a/web/client-v2/src/components/layout/Header/DesktopHeader/DesktopHeader.tsx +++ b/web/client-v2/src/components/layout/Header/DesktopHeader/DesktopHeader.tsx @@ -10,7 +10,7 @@ type DesktopHeaderProps = { user?: User; }; -export const DesktopHeader: FC = async ({ +export const DesktopHeader: FC = ({ user, }: DesktopHeaderProps) => { return ( diff --git a/web/client-v2/src/components/layout/Header/Header.tsx b/web/client-v2/src/components/layout/Header/Header.tsx index 7f9f40b9..4a914329 100644 --- a/web/client-v2/src/components/layout/Header/Header.tsx +++ b/web/client-v2/src/components/layout/Header/Header.tsx @@ -7,7 +7,7 @@ type HeaderProps = { user?: User; }; -export const Header: FC = async ({ user }) => { +export const Header: FC = ({ user }) => { // const myFeedFolderRes = await fetchMyFeedFoldersAPI({}); // const favoriteArticleFolderRes = await fetchFavoriteArticleFoldersAPI({}); return ( diff --git a/web/client-v2/src/components/provider/ApolloProvider.tsx b/web/client-v2/src/components/provider/ApolloProvider.tsx index 2ed019f6..eca11d66 100644 --- a/web/client-v2/src/components/provider/ApolloProvider.tsx +++ b/web/client-v2/src/components/provider/ApolloProvider.tsx @@ -1,12 +1,15 @@ "use client"; import { HttpLink } from "@apollo/client"; +import { setContext } from "@apollo/client/link/context"; import { ApolloNextAppProvider, ApolloClient, InMemoryCache, } from "@apollo/experimental-nextjs-app-support"; +import { getSession } from "@/features/auth/actions/auth"; + function makeClient() { const graphqlUrl = process.env.BFF_API_URL || "http://localhost:3000"; const httpLink = new HttpLink({ @@ -14,9 +17,22 @@ function makeClient() { fetchOptions: { cache: "no-store" }, }); + const authLink = setContext(async (_, { headers }) => { + const { data: session } = await getSession(); + + const token = session.session?.access_token; + + return { + headers: { + ...headers, + authorization: token ? `Bearer ${token}` : "", + }, + }; + }); + return new ApolloClient({ cache: new InMemoryCache(), - link: httpLink, + link: authLink.concat(httpLink), }); } diff --git a/web/client-v2/src/features/articles/components/Template/ArticleDashboardTemplate.tsx b/web/client-v2/src/features/articles/components/Template/ArticleDashboardTemplate.tsx index f04efa1b..2ada0fc7 100644 --- a/web/client-v2/src/features/articles/components/Template/ArticleDashboardTemplate.tsx +++ b/web/client-v2/src/features/articles/components/Template/ArticleDashboardTemplate.tsx @@ -50,5 +50,7 @@ export const ArticleDashboardTemplate: FC< if (error) { return
{error.message}
; } + console.log(data); + return
Article Dashboard
; }; diff --git a/web/client-v2/src/features/auth/actions/auth.ts b/web/client-v2/src/features/auth/actions/auth.ts index 7e44f664..2a585ff1 100644 --- a/web/client-v2/src/features/auth/actions/auth.ts +++ b/web/client-v2/src/features/auth/actions/auth.ts @@ -2,7 +2,10 @@ import { redirect } from "next/navigation"; -import { createServerSideClient } from "@/lib/supabase/client/serverClient"; +import { + createGetOnlyServerSideClient, + createServerSideClient, +} from "@/lib/supabase/client/serverClient"; export async function loginWithGoogle() { const supabase = await createServerSideClient(); @@ -38,3 +41,10 @@ export async function logoutToLoginPage() { } if (!error) redirect("/login"); } + +export async function getSession() { + const supabase = await createGetOnlyServerSideClient(); + const session = supabase.auth.getSession(); + + return session; +} diff --git a/web/client-v2/src/lib/apollo/client.ts b/web/client-v2/src/lib/apollo/client.ts index b207f5e3..0447936e 100644 --- a/web/client-v2/src/lib/apollo/client.ts +++ b/web/client-v2/src/lib/apollo/client.ts @@ -1,17 +1,36 @@ import { HttpLink } from "@apollo/client"; +import { setContext } from "@apollo/client/link/context"; import { registerApolloClient, ApolloClient, InMemoryCache, } from "@apollo/experimental-nextjs-app-support"; +import { getSession } from "@/features/auth/actions/auth"; + +const graphqlUrl = process.env.BFF_API_URL || "http://localhost:3000"; + +const httpLink = new HttpLink({ + uri: `${graphqlUrl}/graphql`, + fetchOptions: { cache: "no-store" }, +}); + +const authLink = setContext(async (_, { headers }) => { + const { data: session } = await getSession(); + + const token = session.session?.access_token; + + return { + headers: { + ...headers, + authorization: `Bearer ${token}`, + }, + }; +}); + export const { getClient } = registerApolloClient(() => { - const graphqlUrl = process.env.BFF_API_URL || "http://localhost:3000"; return new ApolloClient({ cache: new InMemoryCache(), - link: new HttpLink({ - uri: `${graphqlUrl}/graphql`, - fetchOptions: { cache: "no-store" }, - }), + link: authLink.concat(httpLink), }); }); From 31de4ab41e4a45cdcdafc29e716c096bb3daee0d Mon Sep 17 00:00:00 2001 From: YukiOnishi <58220747+YukiOnishi1129@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:52:25 +0900 Subject: [PATCH 2/2] set bff auth guard --- bff/apollo-gateway/package-lock.json | 84 ++++++++++++++++++- bff/apollo-gateway/package.json | 2 + .../src/app/article/article.resolver.ts | 10 ++- .../src/app/article/article.service.ts | 8 +- bff/apollo-gateway/src/app/auth/auth.guard.ts | 40 +++++++++ .../src/graphql/context.interface.ts | 6 ++ bff/apollo-gateway/src/main.ts | 6 +- compose.yml | 3 + 8 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 bff/apollo-gateway/src/app/auth/auth.guard.ts create mode 100644 bff/apollo-gateway/src/graphql/context.interface.ts diff --git a/bff/apollo-gateway/package-lock.json b/bff/apollo-gateway/package-lock.json index 83e483be..8e49867f 100644 --- a/bff/apollo-gateway/package-lock.json +++ b/bff/apollo-gateway/package-lock.json @@ -19,7 +19,9 @@ "@nestjs/mapped-types": "*", "@nestjs/microservices": "^10.4.1", "@nestjs/platform-express": "^10.0.0", + "@supabase/supabase-js": "^2.45.3", "apollo-server-express": "^3.13.0", + "dotenv": "^16.4.5", "google-protobuf": "^3.21.4", "graphql": "^16.9.0", "graphql-type-json": "^0.3.2", @@ -5538,6 +5540,80 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@supabase/auth-js": { + "version": "2.65.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.65.0.tgz", + "integrity": "sha512-+wboHfZufAE2Y612OsKeVP4rVOeGZzzMLD/Ac3HrTQkkY4qXNjI6Af9gtmxwccE5nFvTiF114FEbIQ1hRq5uUw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.1.tgz", + "integrity": "sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.8.tgz", + "integrity": "sha512-YunjXpoQjQ0a0/7vGAvGZA2dlMABXFdVI/8TuVKtlePxyT71sl6ERl6ay1fmIeZcqxiuFQuZw/LXUuStUG9bbg==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.10.2.tgz", + "integrity": "sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.14.2" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.0.tgz", + "integrity": "sha512-iZenEdO6Mx9iTR6T7wC7sk6KKsoDPLq8rdu5VRy7+JiT1i8fnqfcOr6mfF2Eaqky9VQzhP8zZKQYjzozB65Rig==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.45.3", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.45.3.tgz", + "integrity": "sha512-4wAux6cuVMrdH/qUjKn6p3p3L9AtAO3Une6ojIrtpCj1RaXKVoyIATiacSRAI+pKff6XZBVCGC29v+z4Jo/uSw==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.65.0", + "@supabase/functions-js": "2.4.1", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.15.8", + "@supabase/realtime-js": "2.10.2", + "@supabase/storage-js": "2.7.0" + } + }, "node_modules/@ts-morph/common": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.22.0.tgz", @@ -5828,6 +5904,12 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.5.tgz", + "integrity": "sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==", + "license": "MIT" + }, "node_modules/@types/qs": { "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", @@ -5895,7 +5977,6 @@ "version": "8.5.12", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -8732,7 +8813,6 @@ "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" diff --git a/bff/apollo-gateway/package.json b/bff/apollo-gateway/package.json index b6304c4e..04c241ac 100644 --- a/bff/apollo-gateway/package.json +++ b/bff/apollo-gateway/package.json @@ -33,7 +33,9 @@ "@nestjs/mapped-types": "*", "@nestjs/microservices": "^10.4.1", "@nestjs/platform-express": "^10.0.0", + "@supabase/supabase-js": "^2.45.3", "apollo-server-express": "^3.13.0", + "dotenv": "^16.4.5", "google-protobuf": "^3.21.4", "graphql": "^16.9.0", "graphql-type-json": "^0.3.2", diff --git a/bff/apollo-gateway/src/app/article/article.resolver.ts b/bff/apollo-gateway/src/app/article/article.resolver.ts index 2ad067c9..c9c74687 100644 --- a/bff/apollo-gateway/src/app/article/article.resolver.ts +++ b/bff/apollo-gateway/src/app/article/article.resolver.ts @@ -1,7 +1,10 @@ -import { Resolver, Args, Query } from '@nestjs/graphql'; +import { UseGuards } from '@nestjs/common'; +import { Resolver, Args, Query, Context } from '@nestjs/graphql'; import { ArticleService } from './article.service'; +import { GraphQLContext } from '../../graphql/context.interface'; import { ArticleConnection, ArticlesInput } from '../../graphql/types/graphql'; +import { SupabaseAuthGuard } from '../auth/auth.guard'; @Resolver() export class ArticleResolver { @@ -13,10 +16,13 @@ export class ArticleResolver { // } @Query(() => ArticleConnection) + @UseGuards(SupabaseAuthGuard) async articles( @Args('articlesInput') articlesInput: ArticlesInput, + @Context() context: GraphQLContext, ): Promise { - return await this.articleService.getArticles(articlesInput); + const user = context.req.user; + return await this.articleService.getArticles(user.id, articlesInput); } // @Query('article') diff --git a/bff/apollo-gateway/src/app/article/article.service.ts b/bff/apollo-gateway/src/app/article/article.service.ts index 791b99bd..907c93fa 100644 --- a/bff/apollo-gateway/src/app/article/article.service.ts +++ b/bff/apollo-gateway/src/app/article/article.service.ts @@ -29,7 +29,10 @@ export class ArticleService implements OnModuleInit { // return `This action adds a new article ${createArticleInput}`; // } - async getArticles(input: ArticlesInput): Promise { + async getArticles( + userId: string, + input: ArticlesInput, + ): Promise { const req = new GetArticlesRequest(); if (input?.first) req.setLimit(input.first); if (input?.after) req.setCursor(input.after); @@ -44,7 +47,8 @@ export class ArticleService implements OnModuleInit { if (input?.languageStatus) req.setLanguageStatus(new Int64Value().setValue(input.languageStatus)); if (input?.tag) req.setTag(new StringValue().setValue(input.tag)); - if (input?.userId) req.setUserId(new StringValue().setValue(input.userId)); + + req.setUserId(new StringValue().setValue(userId)); return new Promise((resolve, reject) => { this.articleService.getArticles(req, (err, res) => { diff --git a/bff/apollo-gateway/src/app/auth/auth.guard.ts b/bff/apollo-gateway/src/app/auth/auth.guard.ts new file mode 100644 index 00000000..92bcf698 --- /dev/null +++ b/bff/apollo-gateway/src/app/auth/auth.guard.ts @@ -0,0 +1,40 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, +} from '@nestjs/common'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import { createClient } from '@supabase/supabase-js'; + +@Injectable() +export class SupabaseAuthGuard implements CanActivate { + private supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_ANON_KEY, + ); + + async canActivate(context: ExecutionContext): Promise { + const ctx = GqlExecutionContext.create(context).getContext(); + const req = ctx.req || context.switchToHttp().getRequest(); + + const authHeader = req.headers.authorization; + const token = authHeader?.split(' ')[1]; + + if (!token) { + throw new UnauthorizedException('Authorization token not found'); + } + + const { + data: { user }, + error, + } = await this.supabase.auth.getUser(token); + + if (error || !user) { + throw new UnauthorizedException('Invalid or expired token'); + } + + req.user = user; + return true; + } +} diff --git a/bff/apollo-gateway/src/graphql/context.interface.ts b/bff/apollo-gateway/src/graphql/context.interface.ts new file mode 100644 index 00000000..3e69ed61 --- /dev/null +++ b/bff/apollo-gateway/src/graphql/context.interface.ts @@ -0,0 +1,6 @@ +import { User } from '@supabase/supabase-js'; +import { Request } from 'express'; + +export interface GraphQLContext { + req: Request & { user?: User }; +} diff --git a/bff/apollo-gateway/src/main.ts b/bff/apollo-gateway/src/main.ts index bcc101e0..2b267126 100644 --- a/bff/apollo-gateway/src/main.ts +++ b/bff/apollo-gateway/src/main.ts @@ -1,9 +1,13 @@ import { NestFactory } from '@nestjs/core'; +import * as dotenv from 'dotenv'; import { AppModule } from './app/app.module'; +dotenv.config(); + async function bootstrap() { const app = await NestFactory.create(AppModule); - await app.listen(3000); + const port = Number(process.env.PORT) || 3000; + await app.listen(port); } bootstrap(); diff --git a/compose.yml b/compose.yml index a3b66a94..ff97716d 100644 --- a/compose.yml +++ b/compose.yml @@ -7,6 +7,9 @@ services: context: . dockerfile: ./bff/apollo-gateway/Dockerfile.dev environment: + - BFF_CONTAINER_PORT=${BFF_CONTAINER_PORT} + - SUPABASE_URL=${BFF_SUPABASE_URL} + - SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY} - TZ=Asia/Tokyo volumes: - ./bff/apollo-gateway:/app