Skip to content

Commit

Permalink
Handle OAuth error (#299)
Browse files Browse the repository at this point in the history
[frontend]
* Create an API Route on the frontend side that receives oauth(signup) callbacks and calls the backend API
* Create an API Route on the frontend side that receives oauth(login) callbacks and calls the backend API
* Add query to redirect to /signup or /login in case of OAuth failure
* Added toast display when using useSearchParams to get error status and message

[backend]
* Create function that only redirects to frontend with authorization code
  • Loading branch information
lim396 authored Mar 7, 2024
1 parent bba7fb3 commit 8fbacb0
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 9 deletions.
26 changes: 18 additions & 8 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
Post,
Query,
Redirect,
Res,
UseGuards,
} from '@nestjs/common';
import {
Expand All @@ -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';
Expand Down Expand Up @@ -64,7 +62,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);
}
Expand All @@ -79,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')
Expand Down
34 changes: 34 additions & 0 deletions frontend/app/callback/auth/login/oauth2/42/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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}`,
{
headers: {
"Content-Type": "application/json",
},
},
);
const data = await res.json();
if (!res.ok) {
console.error("Failed to authenticate", data);
const params = new URLSearchParams({
status: data.statusCode,
message: data.message,
});
const query = "?" + params.toString();
redirect("/login" + query);
} else {
setAccessToken(data.accessToken);
redirect("/");
}
}
32 changes: 32 additions & 0 deletions frontend/app/callback/auth/signup/oauth2/42/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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);
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", data);
const params = new URLSearchParams({
status: data.statusCode,
message: data.message,
});
const query = "?" + params.toString();
redirect("/signup" + query);
} else {
redirect("/login");
}
}
47 changes: 46 additions & 1 deletion frontend/app/ui/auth/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
50 changes: 50 additions & 0 deletions frontend/app/ui/auth/signup-form.tsx
Original file line number Diff line number Diff line change
@@ -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>
Expand Down

0 comments on commit 8fbacb0

Please sign in to comment.