From 2adafd3b2cae82f081c3dafa1d871e5de955e9af Mon Sep 17 00:00:00 2001 From: lim <lbooks424@gmail.com> Date: Tue, 5 Mar 2024 23:35:35 +0900 Subject: [PATCH 1/6] [frontend] Create an API Route on the frontend side that receives oauth(signup) callbacks and calls the backend API --- .../callback/auth/signup/oauth2/42/route.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 frontend/app/callback/auth/signup/oauth2/42/route.ts diff --git a/frontend/app/callback/auth/signup/oauth2/42/route.ts b/frontend/app/callback/auth/signup/oauth2/42/route.ts new file mode 100644 index 00000000..a320a6bd --- /dev/null +++ b/frontend/app/callback/auth/signup/oauth2/42/route.ts @@ -0,0 +1,25 @@ +import { redirect } from "next/navigation"; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + console.error("Route handler called"); + console.error( + "fetching", + `${process.env.API_URL}/auth/signup/oauth2/42/authenticate?${searchParams}`, + ); + const res = await fetch( + `${process.env.API_URL}/auth/signup/oauth2/42/authenticate?${searchParams}`, + { + headers: { + "Content-Type": "application/json", + }, + }, + ); + const data = await res.json(); + if (!res.ok) { + console.error("Failed to authenticate", res); + console.error("Failed to authenticate", data); + redirect("/signup"); + } + redirect("/login"); +} From b354a4488cb144ffcb358161ce3098d8d1e48864 Mon Sep 17 00:00:00 2001 From: lim <lbooks424@gmail.com> Date: Tue, 5 Mar 2024 23:40:12 +0900 Subject: [PATCH 2/6] [backend] Create function that only redirects to frontend with authorization code --- backend/src/auth/auth.controller.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 31708d23..da38e8e6 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -64,7 +64,14 @@ export class AuthController { @Get('signup/oauth2/42/callback') @ApiOkResponse({ type: AuthEntity }) - @Redirect('/login') + @Redirect() + async signupWithOauth42Callback(@Query('code') code: string) { + // only redirect to the frontend with the code in the query + return { url: `/callback/auth/signup/oauth2/42?code=${code}` }; + } + + @Get('signup/oauth2/42/authenticate') + @ApiOkResponse({ type: AuthEntity }) async signupWithOauth42(@Query('code') code: string) { return this.authService.signupWithOauth42(code); } From beecf91eedf7005b7989cc88c3382e41c542bf67 Mon Sep 17 00:00:00 2001 From: lim <lbooks424@gmail.com> Date: Wed, 6 Mar 2024 01:15:15 +0900 Subject: [PATCH 3/6] [frontend] Create an API Route on the frontend side that receives oauth(login) callbacks and calls the backend API --- .../callback/auth/login/oauth2/42/route.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 frontend/app/callback/auth/login/oauth2/42/route.ts diff --git a/frontend/app/callback/auth/login/oauth2/42/route.ts b/frontend/app/callback/auth/login/oauth2/42/route.ts new file mode 100644 index 00000000..2f07706f --- /dev/null +++ b/frontend/app/callback/auth/login/oauth2/42/route.ts @@ -0,0 +1,22 @@ +import { redirect } from "next/navigation"; +import { setAccessToken } from "@/app/lib/session"; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const res = await fetch( + `${process.env.API_URL}/auth/login/oauth2/42/authenticate?${searchParams}`, + { + headers: { + "Content-Type": "application/json", + }, + }, + ); + const data = await res.json(); + if (!res.ok) { + console.error("Failed to authenticate", res); + console.error("Failed to authenticate", data); + redirect("/login"); + } + setAccessToken(data.accessToken); + redirect("/"); +} From ef506534e6a5481cafecc25bb3e05c9ab0f04683 Mon Sep 17 00:00:00 2001 From: lim <lbooks424@gmail.com> Date: Wed, 6 Mar 2024 01:25:22 +0900 Subject: [PATCH 4/6] [backend] Create function that only redirects to frontend with authorization code --- backend/src/auth/auth.controller.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index da38e8e6..5d4e02b7 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -7,7 +7,6 @@ import { Post, Query, Redirect, - Res, UseGuards, } from '@nestjs/common'; import { @@ -17,7 +16,6 @@ import { ApiTags, } from '@nestjs/swagger'; import type { User } from '@prisma/client'; -import { Response } from 'express'; import { CurrentUser } from 'src/common/decorators/current-user.decorator'; import { AuthService } from './auth.service'; import { LoginDto } from './dto/login.dto'; @@ -86,11 +84,16 @@ export class AuthController { @Get('login/oauth2/42/callback') @ApiOkResponse({ type: AuthEntity }) - loginWithOauth42(@Query('code') code: string, @Res() res: Response) { - return this.authService.loginWithOauth42(code).then((auth) => { - res.cookie('token', auth.accessToken); - res.redirect('/'); - }); + @Redirect() + loginWithOauth42Callback(@Query('code') code: string) { + // only redirect to the frontend with the code in the query + return { url: `/callback/auth/login/oauth2/42?code=${code}` }; + } + + @Get('login/oauth2/42/authenticate') + @ApiOkResponse({ type: AuthEntity }) + loginWithOauth42(@Query('code') code: string) { + return this.authService.loginWithOauth42(code); } @Post('2fa/generate') From a7159540277bc2e64d6f7deb5280988909f01caa Mon Sep 17 00:00:00 2001 From: lim <lbooks424@gmail.com> Date: Wed, 6 Mar 2024 18:50:24 +0900 Subject: [PATCH 5/6] [frontend] Add query to redirect in case of OAuth failure --- .../callback/auth/login/oauth2/42/route.ts | 22 ++++++++++++++---- .../callback/auth/signup/oauth2/42/route.ts | 23 ++++++++++++------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/frontend/app/callback/auth/login/oauth2/42/route.ts b/frontend/app/callback/auth/login/oauth2/42/route.ts index 2f07706f..b6cd28f7 100644 --- a/frontend/app/callback/auth/login/oauth2/42/route.ts +++ b/frontend/app/callback/auth/login/oauth2/42/route.ts @@ -1,7 +1,14 @@ -import { redirect } from "next/navigation"; import { setAccessToken } from "@/app/lib/session"; +import { redirect } from "next/navigation"; + +const isValidUrl = (url: string) => { + return URL.canParse(url); +}; export async function GET(request: Request) { + if (!isValidUrl(request.url)) { + redirect("/login"); + } const { searchParams } = new URL(request.url); const res = await fetch( `${process.env.API_URL}/auth/login/oauth2/42/authenticate?${searchParams}`, @@ -13,10 +20,15 @@ export async function GET(request: Request) { ); const data = await res.json(); if (!res.ok) { - console.error("Failed to authenticate", res); console.error("Failed to authenticate", data); - redirect("/login"); + const params = new URLSearchParams({ + status: data.statusCode, + message: data.message, + }); + const query = "?" + params.toString(); + redirect("/login" + query); + } else { + setAccessToken(data.accessToken); + redirect("/"); } - setAccessToken(data.accessToken); - redirect("/"); } diff --git a/frontend/app/callback/auth/signup/oauth2/42/route.ts b/frontend/app/callback/auth/signup/oauth2/42/route.ts index a320a6bd..c698906a 100644 --- a/frontend/app/callback/auth/signup/oauth2/42/route.ts +++ b/frontend/app/callback/auth/signup/oauth2/42/route.ts @@ -1,12 +1,14 @@ import { redirect } from "next/navigation"; +const isValidUrl = (url: string) => { + return URL.canParse(url); +}; + export async function GET(request: Request) { + if (!isValidUrl(request.url)) { + redirect("/signup"); + } const { searchParams } = new URL(request.url); - console.error("Route handler called"); - console.error( - "fetching", - `${process.env.API_URL}/auth/signup/oauth2/42/authenticate?${searchParams}`, - ); const res = await fetch( `${process.env.API_URL}/auth/signup/oauth2/42/authenticate?${searchParams}`, { @@ -17,9 +19,14 @@ export async function GET(request: Request) { ); const data = await res.json(); if (!res.ok) { - console.error("Failed to authenticate", res); console.error("Failed to authenticate", data); - redirect("/signup"); + const params = new URLSearchParams({ + status: data.statusCode, + message: data.message, + }); + const query = "?" + params.toString(); + redirect("/signup" + query); + } else { + redirect("/login"); } - redirect("/login"); } From 61d29f2be9477e8cb25e9a7974ed27c471843f44 Mon Sep 17 00:00:00 2001 From: lim <lbooks424@gmail.com> Date: Wed, 6 Mar 2024 18:57:47 +0900 Subject: [PATCH 6/6] [frontend] Added toast display when using useSearchParams to get error status and message --- frontend/app/ui/auth/login-form.tsx | 47 +++++++++++++++++++++++++- frontend/app/ui/auth/signup-form.tsx | 50 ++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/frontend/app/ui/auth/login-form.tsx b/frontend/app/ui/auth/login-form.tsx index 78073afb..0a128744 100644 --- a/frontend/app/ui/auth/login-form.tsx +++ b/frontend/app/ui/auth/login-form.tsx @@ -7,11 +7,56 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; -import { useRouter } from "next/navigation"; +import { toast } from "@/components/ui/use-toast"; +import { useRouter, useSearchParams } from "next/navigation"; import { useEffect } from "react"; import { useFormState, useFormStatus } from "react-dom"; +const getStatusText = (statusCode: string) => { + let statusText = ""; + switch (statusCode) { + case "400": + statusText = "Bad Request"; + break; + case "401": + statusText = "Unauthorized"; + break; + case "403": + statusText = "Forbidden"; + break; + case "404": + statusText = "Not Found"; + break; + case "409": + statusText = "Conflict"; + break; + case "500": + statusText = "Internal Server Error"; + break; + default: + statusText = "Error"; + break; + } + return statusText; +}; + +const showErrorToast = (statusCode: string, message: string) => { + const statusText = getStatusText(statusCode); + toast({ + title: statusCode + " " + statusText, + description: message, + }); +}; + export default function LoginForm() { + const searchParams = useSearchParams(); + const errorStatusCode = searchParams.get("status"); + const errorMessage = searchParams.get("message"); + useEffect(() => { + if (errorStatusCode && errorMessage) { + showErrorToast(errorStatusCode, errorMessage); + } + }, [errorStatusCode, errorMessage]); return ( <> <Card> diff --git a/frontend/app/ui/auth/signup-form.tsx b/frontend/app/ui/auth/signup-form.tsx index 6ab3c752..d9a89be2 100644 --- a/frontend/app/ui/auth/signup-form.tsx +++ b/frontend/app/ui/auth/signup-form.tsx @@ -1,9 +1,59 @@ +"use client"; + import Form from "@/app/ui/user/create-form"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; +import { toast } from "@/components/ui/use-toast"; +import { useSearchParams } from "next/navigation"; +import { useEffect } from "react"; + +const getStatusText = (statusCode: string) => { + let statusText = ""; + switch (statusCode) { + case "400": + statusText = "Bad Request"; + break; + case "401": + statusText = "Unauthorized"; + break; + case "403": + statusText = "Forbidden"; + break; + case "404": + statusText = "Not Found"; + break; + case "409": + statusText = "Conflict"; + break; + case "500": + statusText = "Internal Server Error"; + break; + default: + statusText = "Error"; + break; + } + return statusText; +}; + +const showErrorToast = (statusCode: string, message: string) => { + const statusText = getStatusText(statusCode); + toast({ + title: statusCode + " " + statusText, + description: message, + }); +}; export default function SignUpForm() { + const searchParams = useSearchParams(); + const errorStatusCode = searchParams.get("status"); + const errorMessage = searchParams.get("message"); + + useEffect(() => { + if (errorStatusCode && errorMessage) { + showErrorToast(errorStatusCode, errorMessage); + } + }, [errorStatusCode, errorMessage]); return ( <Card> <CardHeader>