fix: include a link and more useful error details for 403 response codes (#16644)

Currently if a user gets to a page they don't have permission to view
they're greeted with a vague error alert and no actionable items. This
PR adds a link back to _/workspaces_ within the alert as well as more
helpful error details.

Before:
![Screenshot 2025-02-20 at 11 06
06 AM](https://github.com/user-attachments/assets/cea5b86d-673b-482b-ac0b-f132eb518910)

After:
![Screenshot 2025-02-20 at 11 06
19 AM](https://github.com/user-attachments/assets/6bf0e9fd-fc51-4d9a-afbc-fea9f0439aff)
This commit is contained in:
brettkolodny
2025-02-21 16:26:07 -05:00
committed by GitHub
parent f8a49f4984
commit a376e8dbfe
7 changed files with 48 additions and 23 deletions
+1
View File
@@ -154,6 +154,7 @@ func ResourceNotFound(rw http.ResponseWriter) {
func Forbidden(rw http.ResponseWriter) {
Write(context.Background(), rw, http.StatusForbidden, codersdk.Response{
Message: "Forbidden.",
Detail: "You don't have permission to view this content. If you believe this is a mistake, please contact your administrator or try signing in with different credentials.",
})
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
import { API } from "api/api";
import { Language } from "pages/CreateUserPage/CreateUserForm";
import { Language } from "pages/CreateUserPage/Language";
import { coderPort, license, premiumTestsRequired, users } from "../constants";
import { expectUrl } from "../expectUrl";
import { createUser } from "../helpers";
+8
View File
@@ -133,6 +133,14 @@ export const getErrorDetail = (error: unknown): string | undefined => {
return undefined;
};
export const getErrorStatus = (error: unknown): number | undefined => {
if (isApiError(error)) {
return error.status;
}
return undefined;
};
export class DetailedError extends Error {
constructor(
message: string,
+25 -9
View File
@@ -1,6 +1,7 @@
import AlertTitle from "@mui/material/AlertTitle";
import { getErrorDetail, getErrorMessage } from "api/errors";
import { getErrorDetail, getErrorMessage, getErrorStatus } from "api/errors";
import type { FC } from "react";
import { Link } from "../Link/Link";
import { Alert, AlertDetail, type AlertProps } from "./Alert";
export const ErrorAlert: FC<
@@ -8,6 +9,7 @@ export const ErrorAlert: FC<
> = ({ error, ...alertProps }) => {
const message = getErrorMessage(error, "Something went wrong.");
const detail = getErrorDetail(error);
const status = getErrorStatus(error);
// For some reason, the message and detail can be the same on the BE, but does
// not make sense in the FE to showing them duplicated
@@ -15,14 +17,28 @@ export const ErrorAlert: FC<
return (
<Alert severity="error" {...alertProps}>
{detail ? (
<>
<AlertTitle>{message}</AlertTitle>
{shouldDisplayDetail && <AlertDetail>{detail}</AlertDetail>}
</>
) : (
message
)}
{
// When the error is a Forbidden response we include a link for the user to
// go back to a known viewable page.
status === 403 ? (
<>
<AlertTitle>{message}</AlertTitle>
<AlertDetail>
{detail}{" "}
<Link href="/workspaces" className="w-fit">
Go to workspaces
</Link>
</AlertDetail>
</>
) : detail ? (
<>
<AlertTitle>{message}</AlertTitle>
{shouldDisplayDetail && <AlertDetail>{detail}</AlertDetail>}
</>
) : (
message
)
}
</Alert>
);
};
@@ -19,18 +19,7 @@ import {
onChangeTrimmed,
} from "utils/formUtils";
import * as Yup from "yup";
export const Language = {
emailLabel: "Email",
passwordLabel: "Password",
usernameLabel: "Username",
nameLabel: "Full name",
emailInvalid: "Please enter a valid email address.",
emailRequired: "Please enter an email address.",
passwordRequired: "Please enter a password.",
createUser: "Create",
cancel: "Cancel",
};
import { Language } from "./Language";
export const authMethodLanguage = {
password: {
@@ -4,8 +4,8 @@ import {
renderWithAuth,
waitForLoaderToBeRemoved,
} from "testHelpers/renderHelpers";
import { Language as FormLanguage } from "./CreateUserForm";
import { CreateUserPage } from "./CreateUserPage";
import { Language as FormLanguage } from "./Language";
const renderCreateUserPage = async () => {
renderWithAuth(<CreateUserPage />, {
+11
View File
@@ -0,0 +1,11 @@
export const Language = {
emailLabel: "Email",
passwordLabel: "Password",
usernameLabel: "Username",
nameLabel: "Full name",
emailInvalid: "Please enter a valid email address.",
emailRequired: "Please enter an email address.",
passwordRequired: "Please enter a password.",
createUser: "Create",
cancel: "Cancel",
};