Skip to content

Commit

Permalink
Merge pull request #319 from tylerslaton/fix-auth-edges
Browse files Browse the repository at this point in the history
feat: add error boundary
  • Loading branch information
tylerslaton authored Oct 25, 2024
2 parents 34afc8b + a7fda1d commit 43a4329
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 75 deletions.
49 changes: 49 additions & 0 deletions ui/admin/app/components/errors/Error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Link } from "@remix-run/react";
import { ArrowLeft, HomeIcon } from "lucide-react";

import { OttoLogo } from "~/components/branding/OttoLogo";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "~/components/ui/card";

export function Error({ error }: { error: Error }) {
return (
<div className="flex min-h-screen w-full items-center justify-center p-4">
<Card className="w-96">
<CardHeader className="mx-4">
<OttoLogo classNames={{ image: "h-10 w-10" }} />
</CardHeader>
<CardContent className="space-y-2 text-center border-b mb-4">
<CardTitle>Oops! An error occurred</CardTitle>
<CardDescription>{error.message}</CardDescription>
<p className="text-sm text-muted-foreground">
Please try again later or contact support if the problem
persists.
</p>
</CardContent>
<CardFooter className="flex gap-4">
<Button className="w-full" variant="secondary" asChild>
<Link to="/">
<HomeIcon className="mr-2" /> Go home
</Link>
</Button>
<Button
className="w-full"
variant="secondary"
onClick={() => {
window.location.reload();
}}
>
<ArrowLeft className="mr-2" /> Go back
</Button>
</CardFooter>
</Card>
</div>
);
}
40 changes: 40 additions & 0 deletions ui/admin/app/components/errors/RouteError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ErrorResponse } from "@remix-run/react";

import { OttoLogo } from "~/components/branding/OttoLogo";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "~/components/ui/card";

export function RouteError({ error }: { error: ErrorResponse }) {
return (
<div className="flex min-h-screen w-full items-center justify-center p-4">
<Card className="w-96">
<CardHeader className="mx-4">
<OttoLogo classNames={{ image: "h-10 w-10" }} />
</CardHeader>
<CardContent className="space-y-2 text-center border-b mb-4">
<CardTitle>Oops! {error.status}</CardTitle>
<CardDescription>{error.statusText}</CardDescription>
<p className="text-sm text-muted-foreground">
{error.data}
</p>
</CardContent>
<CardFooter>
<Button
className="w-full"
variant="secondary"
onClick={() => window.location.reload()}
>
Try Again
</Button>
</CardFooter>
</Card>
</div>
);
}
40 changes: 40 additions & 0 deletions ui/admin/app/components/errors/Unauthorized.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { OttoLogo } from "~/components/branding/OttoLogo";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
} from "~/components/ui/card";

export function Unauthorized() {
return (
<div className="flex min-h-screen w-full items-center justify-center p-4">
<Card className="w-96">
<CardHeader className="mx-4">
<OttoLogo classNames={{ image: "h-10 w-10" }} />
</CardHeader>
<CardContent className="space-y-2 text-center border-b mb-4">
<CardDescription className="text-center">
You are not authorized to access this page. Please sign
in with an authorized account or contact your
administrator.
</CardDescription>
</CardContent>
<CardFooter>
<Button
className="w-full"
variant="secondary"
onClick={() => {
window.location.href =
"/oauth2/sign_out?rd=/admin/";
}}
>
Sign Out
</Button>
</CardFooter>
</Card>
</div>
);
}
3 changes: 3 additions & 0 deletions ui/admin/app/components/errors/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { Error } from "~/components/errors/Error";
export { Unauthorized } from "~/components/errors/Unauthorized";
export { RouteError } from "~/components/errors/RouteError";
55 changes: 28 additions & 27 deletions ui/admin/app/components/signin/SignIn.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from "react";
import { FaGoogle } from "react-icons/fa";

import { cn } from "~/lib/utils";
Expand All @@ -17,31 +16,33 @@ interface SignInProps {
className?: string;
}

const SignIn: React.FC<SignInProps> = ({ className }) => {
export function SignIn({ className }: SignInProps) {
return (
<Card className={cn("flex flex-col justify-between", className)}>
<CardHeader>
<CardTitle className="flex items-center justify-center">
<OttoLogo />
</CardTitle>
<CardDescription className="text-center w-3/4 mx-auto pt-4">
Please sign in using the button below.
</CardDescription>
</CardHeader>
<CardFooter className="border-t pt-4">
<Button
variant="secondary"
className="w-full"
onClick={() => {
window.location.href = "/oauth2/start?rd=/admin/";
}}
>
<FaGoogle className="mr-2" />
Sign In with Google
</Button>
</CardFooter>
</Card>
<div className="flex min-h-screen w-full items-center justify-center p-4">
<Card
className={cn("flex flex-col justify-between w-96", className)}
>
<CardHeader>
<CardTitle className="flex items-center justify-center">
<OttoLogo />
</CardTitle>
<CardDescription className="text-center w-3/4 mx-auto pt-4">
Please sign in using the button below.
</CardDescription>
</CardHeader>
<CardFooter className="border-t pt-4">
<Button
variant="secondary"
className="w-full"
onClick={() => {
window.location.href = "/oauth2/start?rd=/admin/";
}}
>
<FaGoogle className="mr-2" />
Sign In with Google
</Button>
</CardFooter>
</Card>
</div>
);
};

export default SignIn;
}
31 changes: 6 additions & 25 deletions ui/admin/app/lib/service/api/primitives.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// TODO: Add default configurations with auth tokens, etc. When ready
import axios, { AxiosRequestConfig, AxiosResponse, isAxiosError } from "axios";
import { toast } from "sonner";
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";

export const ResponseHeaders = {
RunId: "x-otto-run-id",
Expand All @@ -18,27 +17,9 @@ export async function request<T, R = AxiosResponse<T>, D = unknown>({
errorMessage = "Request failed",
...config
}: ExtendedAxiosRequestConfig<D>): Promise<R> {
try {
return await internalFetch<T, R, D>({
adapter: "fetch",
...config,
});
} catch (error) {
handleRequestError(error, errorMessage);
throw error;
}
}

function handleRequestError(error: unknown, errorMessage: string): void {
if (isAxiosError(error) && error.response) {
const { status, config } = error.response;
const method = config.method?.toUpperCase() || "UNKNOWN";
toast.error(`${status} ${method}`, {
description: errorMessage,
});
} else {
toast.error("Request Error", {
description: errorMessage,
});
}
console.error(errorMessage);
return await internalFetch<T, R, D>({
adapter: "fetch",
...config,
});
}
14 changes: 14 additions & 0 deletions ui/admin/app/lib/service/authService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { AxiosError } from "axios";

import { UserService } from "./api/userService";

export const signedIn = async () => {
try {
await UserService.getMe();
} catch (error) {
if (error instanceof AxiosError && error.response?.status === 403) {
return false;
}
}
return true;
};
38 changes: 24 additions & 14 deletions ui/admin/app/routes/_auth.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
import { Outlet, redirect } from "@remix-run/react";
import { isAxiosError } from "axios";
import { $path } from "remix-routes";

import { UserService } from "~/lib/service/api/userService";
import { Outlet, isRouteErrorResponse, useRouteError } from "@remix-run/react";
import { AxiosError } from "axios";

import { AuthDisabledUsername, useAuth } from "~/components/auth/AuthContext";
import { Error, RouteError, Unauthorized } from "~/components/errors";
import { HeaderNav } from "~/components/header/HeaderNav";
import { Sidebar } from "~/components/sidebar";
import { SignIn } from "~/components/signin/SignIn";

export function ErrorBoundary() {
const error = useRouteError();
const { me } = useAuth();

export const clientLoader = async () => {
try {
await UserService.getMe();
} catch (error) {
if (isAxiosError(error) && error.response?.status === 403) {
throw redirect($path("/sign-in"));
}
switch (true) {
case error instanceof AxiosError:
if (
error.response?.status === 403 &&
me.username &&
me.username !== AuthDisabledUsername
) {
return <Unauthorized />;
}
return <SignIn />;
case isRouteErrorResponse(error):
return <RouteError error={error} />;
default:
return <Error error={error as Error} />;
}
return null;
};
}

export default function AuthLayout() {
return (
Expand Down
9 changes: 0 additions & 9 deletions ui/admin/app/routes/sign-in.tsx

This file was deleted.

0 comments on commit 43a4329

Please sign in to comment.