Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions apps/nowcasting-app/components/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,11 +406,20 @@ export const axiosFetcherAuth = async (url: RequestInfo | URL) => {
try {
const response = await fetch("/api/get_token");
if (!response.ok) {
// Ensure SWR sees an error if token retrieval fails
const text = await response.text().catch(() => "");
throw new Error(
`Failed to get access token (${response.status}): ${text || response.statusText}`
);
const body = await response.json().catch((parseErr) => {
Sentry.captureException(parseErr, { tags: { error: "get_token_parse_failure" } });
return {};
});
if (body.error === "trial_expired") {
Router.push(`/expired${body.email ? `?email=${encodeURIComponent(body.email)}` : ""}`);
throw new Error("trial_expired");
}
if (body.error === "access_denied") {
Router.push(`/auth/denied?error_description=${encodeURIComponent(body.message)}`);
throw new Error("access_denied");
}
const text = body.message || response.statusText;
throw new Error(`Failed to get access token (${response.status}): ${text}`);
}
const { accessToken } = await response.json();

Expand Down
7 changes: 5 additions & 2 deletions apps/nowcasting-app/components/layout/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { OCFlogo } from "../../icons/logo";
import Link from "next/link";
import { Menu } from "@headlessui/react";
import { getViewTitle, VIEWS } from "../../../constant";
import { Dispatch, SetStateAction } from "react";
import { Dispatch, ReactNode, SetStateAction } from "react";
import { ExternalLinkIcon } from "../../icons/icons";
import { CombinedData } from "../../types";

Expand Down Expand Up @@ -82,13 +82,15 @@ type HeaderProps = {
setView: Dispatch<SetStateAction<VIEWS>>;
isLoggedIn?: boolean;
combinedData?: CombinedData | null;
children?: ReactNode;
};

const Header: React.FC<HeaderProps> = ({
view,
setView,
isLoggedIn = true,
combinedData = null
combinedData = null,
children

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is children being used for?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just allows us to inject child components into the Header when needed, e.g. for this PR having a Sign Out on the Blocked/Access Denied screen (in case a user logs in with the wrong email address to let them still sign out from that page)

}) => {
return (
<header className="h-16 text-white text-right sm:px-4 bg-black flex absolute top-0 w-full overflow-y-visible p-1 text-sm items-center z-30">
Expand Down Expand Up @@ -151,6 +153,7 @@ const Header: React.FC<HeaderProps> = ({
<div className="flex items-center gap-2">
<div className="py-1">
{isLoggedIn && <ProfileDropDown view={view} combinedData={combinedData} />}
{children}
</div>
</div>
</header>
Expand Down
5 changes: 3 additions & 2 deletions apps/nowcasting-app/pages/api/auth/[...auth0].ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export default wrapApiHandlerWithSentry(
redirect_uri: redirectUri,
audience: process.env.NEXT_PUBLIC_AUTH0_API_AUDIENCE || "https://api.nowcasting.io/", // Production fallback
scope: "openid profile email offline_access",
useRefreshTokens: true
useRefreshTokens: true,
...(req.query.prompt === "login" && { prompt: "login" })
},
returnTo: returnTo
});
Expand All @@ -62,7 +63,7 @@ export default wrapApiHandlerWithSentry(

async logout(req: NextApiRequest, res: NextApiResponse) {
setUser(null);
const returnTo = req.query.redirectToLogin ? "/api/auth/login" : "/logout";
const returnTo = req.query.redirectToLogin ? "/api/auth/login?prompt=login" : "/logout";
await handleLogout(req, res, {
returnTo
});
Expand Down
10 changes: 9 additions & 1 deletion apps/nowcasting-app/pages/api/get_token.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getAccessToken, withApiAuthRequired } from "@auth0/nextjs-auth0";
import { getAccessToken, getSession, withApiAuthRequired } from "@auth0/nextjs-auth0";
import { NextApiRequest, NextApiResponse } from "next";

export default process.env.NEXT_PUBLIC_DEV_MODE === "true"
Expand All @@ -19,8 +19,16 @@ export default process.env.NEXT_PUBLIC_DEV_MODE === "true"
}
try {
const accessToken = await getAccessToken(req, res);
const session = await getSession(req, res);
const trialEndsAt = session?.user?.trial_ends_at;
if (trialEndsAt && new Date(trialEndsAt) < new Date()) {
return res.status(403).json({ error: "trial_expired", email: session?.user?.email });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to write any unit tests for this? Maybe mocking Auth0 is too hard?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For unit tests yeah definitely too much, but we could try to add a Cypress test for this actually 👍 I'll add to that issue

res.status(200).json(accessToken);
} catch (error: any) {
if (error.message?.includes("access_denied")) {
return res.status(403).json({ error: "access_denied", message: error.message });
}
res.status(error.status || 400).end(error.message);
}
});
88 changes: 59 additions & 29 deletions apps/nowcasting-app/pages/auth/denied.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,46 +21,76 @@ const AccessDeniedPage = ({ query }: { query: any }) => {
document.cookie = "visited_access_denied=true; path=/; max-age=3600"; // 1 hour
}
}, []);
const isEmailVerification = errorDescription?.includes("Email not verified");

return (
<>
<Head>
<title>Email Verification | Quartz Solar UI</title>
<title>
{isEmailVerification ? "Email Verification" : "Access Denied"} | Quartz Solar UI
</title>
<link rel="icon" href="/favicon.ico" />
</Head>

<div className="bg-mapbox-black min-h-screen flex flex-col">
<Header view={VIEWS.FORECAST} setView={() => {}} isLoggedIn={false} />
<main className="w-full px-4 mx-auto max-w-lg sm:px-6 lg:px-8 flex-1 flex flex-col items-center justify-center">
<div className="max-w-xl py-16 mx-auto sm:py-24 text-center gap-6 flex flex-col mt-2 text-lg text-white">
<h1 className="text-4xl font-extrabold tracking-tight text-ocf-gray-500 sm:text-5xl">
Nearly there.
</h1>
<p className="font-light">Please check your email for a verification link.</p>
{hasVisited &&
errorDescription &&
errorDescription?.includes("Email not verified.") && (
<p className="mt-3 bg-ocf-yellow/25 text-xs p-4 rounded-md leading-relaxed">
Hmm, it seems like you haven&apos;t verified your email address yet. <br />
Please check your inbox for a verification link.
{isEmailVerification ? (
<>
<h1 className="text-4xl font-extrabold tracking-tight text-ocf-gray-500 sm:text-5xl">
Nearly there.
</h1>
<p className="font-light">Please check your email for a verification link.</p>
{hasVisited && (
<p className="mt-3 bg-ocf-yellow/25 text-xs p-4 rounded-md leading-relaxed">
Hmm, it seems like you haven&apos;t verified your email address yet. <br />
Please check your inbox for a verification link.
</p>
)}
<Link
href={`/`}
className="text-sm self-center my-3 py-2 px-4 font-medium hover:cursor-pointer bg-ocf-gray-500 hover:bg-ocf-yellow-600 active:bg-ocf-yellow-600 text-black transition-all duration-200 rounded-full"
>
I&apos;ve verified, continue<span aria-hidden="true"> &rarr;</span>
</Link>
<p className="text-sm text-gray-400">
If you think this is a mistake, please contact the Quartz Solar team at{" "}
<a
href="mailto:support@quartz.solar"
className="text-danube-600 underline hover:text-danube-800"
>
support@quartz.solar
</a>
.
</p>
</>
) : (
<>
<h1 className="text-4xl font-extrabold tracking-tight text-ocf-gray-500 sm:text-5xl">
Access denied.
</h1>
<p className="font-light">
Your account does not currently have access to Quartz Solar.
</p>
<p className="text-sm text-gray-400">
If you think this is a mistake, please contact us at{" "}
<a
href="mailto:support@quartz.solar"
className="text-danube-600 underline hover:text-danube-800"
>
support@quartz.solar
</a>
.
</p>
)}
<Link
href={`/`}
className="text-sm self-center my-3 py-2 px-4 font-medium hover:cursor-pointer bg-ocf-gray-500 hover:bg-ocf-yellow-600 active:bg-ocf-yellow-600 text-black transition-all duration-200 rounded-full"
>
I&apos;ve verified, continue<span aria-hidden="true"> &rarr;</span>
</Link>
<p className="text-sm text-gray-400">
If you think this is a mistake, you have verified your email, and should have access,
please contact the Quartz Solar team at{" "}
<a
href="mailto:support@quartz.solar"
className="text-danube-600 underline hover:text-danube-800"
>
support@quartz.solar
</a>
.
</p>
<Link
href="/api/auth/logout?redirectToLogin=true"
className="text-sm self-center my-3 py-2 px-4 font-medium hover:cursor-pointer bg-ocf-gray-500 hover:bg-ocf-yellow-600 active:bg-ocf-yellow-600 text-black transition-all duration-200 rounded-full"
>
Sign out<span aria-hidden="true"> &rarr;</span>
</Link>
</>
)}
</div>
</main>
</div>
Expand Down
9 changes: 8 additions & 1 deletion apps/nowcasting-app/pages/expired.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Head from "next/head";
import { useSearchParams } from "next/navigation";
import Header from "../components/layout/header";
import { VIEWS } from "../constant";
import React from "react";

const TrialExpiredPage = () => {
const queryParams = useSearchParams();
Expand All @@ -17,7 +18,13 @@ const TrialExpiredPage = () => {
</Head>

<div className="bg-mapbox-black min-h-screen flex flex-col">
<Header view={VIEWS.FORECAST} setView={() => {}} isLoggedIn={false} />
<Header view={VIEWS.FORECAST} setView={() => {}} isLoggedIn={false}>
<Link href="/api/auth/logout?redirectToLogin=true" legacyBehavior>
<a id={"UserMenu-LogoutBtn"} className="!text-xs btn btn-outline rounded-md">
Sign out&nbsp;→
</a>
</Link>
</Header>
<main className="w-full px-4 mx-auto max-w-2xl sm:px-6 lg:px-8 flex-1 flex flex-col items-center justify-center">
<div className="max-w-xl py-16 mx-auto sm:py-24 text-center gap-4 flex flex-col mt-2 text-lg text-white">
<p className="font-light">Your Quartz Solar trial has now ended.</p>
Expand Down
4 changes: 4 additions & 0 deletions apps/nowcasting-app/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
@apply inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-mapbox-black bg-ocf-yellow hover:bg-ocf-yellow-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-ocf-delta-500;
}

.btn.btn-outline {
@apply inline-flex items-center justify-center px-4 py-2 border border-ocf-gray-700 rounded-full shadow-sm text-sm font-medium text-ocf-gray-700 bg-transparent hover:text-ocf-yellow hover:border-ocf-yellow focus:outline-none;
}

.pv-map a.mapboxgl-ctrl-logo, .delta-map a.mapboxgl-ctrl-logo {
margin-left: 0.5rem;
}
Expand Down
Loading