Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth Routing #850

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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: 16 additions & 3 deletions frontend2/src/api/auth/authApi.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { TokenApi } from "../_autogen";
import Cookies from "js-cookie";
import { DEFAULT_API_CONFIGURATION } from "../helpers";
import { buildKey, DEFAULT_API_CONFIGURATION } from "../helpers";
import { type QueryClient } from "@tanstack/react-query";
import { userQueryKeys } from "../user/userKeys";
import { loginTokenVerifyFactory } from "api/user/userFactories";

/** This file contains all frontend authentication functions. Responsible for interacting with Cookies and expiring/setting JWT tokens. */
const API = new TokenApi(DEFAULT_API_CONFIGURATION);
Expand All @@ -26,6 +27,12 @@ export const login = async (

Cookies.set("access", res.access);
Cookies.set("refresh", res.refresh);

queryClient.setQueryData<boolean>(
buildKey(loginTokenVerifyFactory.queryKey, { queryClient }),
true,
);

await queryClient.refetchQueries({
// OK to call KEY.key() here as we are refetching all user-me queries.
queryKey: userQueryKeys.meBase.key(),
Expand All @@ -40,8 +47,14 @@ export const login = async (
export const logout = async (queryClient: QueryClient): Promise<void> => {
Cookies.remove("access");
Cookies.remove("refresh");
await queryClient.resetQueries({
// OK to call KEY.key() here as we are resetting all user-me queries.

queryClient.setQueryData<boolean>(
buildKey(loginTokenVerifyFactory.queryKey, { queryClient }),
false,
);

await queryClient.refetchQueries({
// OK to call KEY.key() here as we are refetching all user-me queries.
queryKey: userQueryKeys.meBase.key(),
});
};
Expand Down
15 changes: 11 additions & 4 deletions frontend2/src/api/loaders/homeIfLoggedIn.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import type { QueryClient } from "@tanstack/react-query";
import { loginCheck } from "api/auth/authApi";
import { type LoaderFunction } from "react-router-dom";
import { buildKey } from "api/helpers";
import { loginTokenVerifyFactory } from "api/user/userFactories";
import { redirect, type LoaderFunction } from "react-router-dom";
import { DEFAULT_EPISODE } from "utils/constants";

export const homeIfLoggedIn =
(queryClient: QueryClient): LoaderFunction =>
async () => {
// Check if user is logged in
if (await loginCheck(queryClient)) {
const loggedIn = await queryClient.ensureQueryData<boolean>({
queryKey: buildKey(loginTokenVerifyFactory.queryKey, { queryClient }),
queryFn: async () =>
await loginTokenVerifyFactory.queryFn({ queryClient }),
});

if (loggedIn) {
// If user is logged in, redirect to home
window.location.href = `/${DEFAULT_EPISODE}/home`;
return redirect(`/${DEFAULT_EPISODE}/home`);
}
return null;
};
4 changes: 3 additions & 1 deletion frontend2/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { Fragment } from "react";
import { Menu, Transition } from "@headlessui/react";
import { Link, NavLink } from "react-router-dom";
import { Link, NavLink, useNavigate } from "react-router-dom";
import { AuthStateEnum, useCurrentUser } from "../contexts/CurrentUserContext";
import Icon from "./elements/Icon";
import { useEpisodeId } from "../contexts/EpisodeContext";
Expand All @@ -13,6 +13,7 @@ const Header: React.FC = () => {
const { authState, user } = useCurrentUser();
const { episodeId } = useEpisodeId();
const queryClient = useQueryClient();
const navigate = useNavigate();
return (
<nav className="fixed top-0 z-30 h-16 w-full bg-gray-700">
<div className="w-full px-2 sm:px-6 lg:px-8">
Expand Down Expand Up @@ -117,6 +118,7 @@ const Header: React.FC = () => {
<button
onClick={() => {
void logout(queryClient);
navigate(`/${episodeId}/home`);
}}
className="flex w-full items-center rounded-lg px-4 py-2 sm:text-sm"
>
Expand Down
12 changes: 8 additions & 4 deletions frontend2/src/components/PrivateRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import React from "react";
import React, { useEffect } from "react";
import { AuthStateEnum, useCurrentUser } from "../contexts/CurrentUserContext";
import { Outlet, useNavigate } from "react-router-dom";
import Spinner from "./Spinner";

const PrivateRoute: React.FC = () => {
const { authState } = useCurrentUser();
const navigate = useNavigate();

useEffect(() => {
if (authState === AuthStateEnum.NOT_AUTHENTICATED) {
navigate("/login");
}
}, [navigate, authState]);

if (authState === AuthStateEnum.AUTHENTICATED) {
return <Outlet />;
} else if (authState === AuthStateEnum.NOT_AUTHENTICATED) {
navigate("/login");
return null;
} else {
return (
<div className="flex h-screen items-center justify-center">
Expand Down
1 change: 1 addition & 0 deletions frontend2/src/components/elements/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const VARIANTS = {
"bg-red-50 text-red-800 hover:bg-red-100 hover:ring-red-700 ring-red-500 ring-1 ring-inset",
"light-outline":
"ring-2 ring-inset ring-gray-200 text-gray-200 hover:bg-gray-100/20",
"no-outline": "text-gray-800 px-0 py-0 hover:bg-gray-100/20",
} as const;

type VariantType = keyof typeof VARIANTS;
Expand Down
8 changes: 8 additions & 0 deletions frontend2/src/components/elements/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
InformationCircleIcon as InformationCircleIcon24,
Bars3Icon as Bars3Icon24,
XMarkIcon as XMarkIcon24,
EyeIcon as EyeIcon24,
EyeSlashIcon as EyeSlashIcon24,
} from "@heroicons/react/24/outline";

import {
Expand All @@ -31,6 +33,8 @@ import {
InformationCircleIcon as InformationCircleIcon20,
Bars3Icon as Bars3Icon20,
XMarkIcon as XMarkIcon20,
EyeIcon as EyeIcon20,
EyeSlashIcon as EyeSlashIcon20,
} from "@heroicons/react/20/solid";

const icons24 = {
Expand All @@ -48,6 +52,8 @@ const icons24 = {
information_circle: InformationCircleIcon24,
bars_3: Bars3Icon24,
x_mark: XMarkIcon24,
eye: EyeIcon24,
eye_slash: EyeSlashIcon24,
};

const icons20 = {
Expand All @@ -65,6 +71,8 @@ const icons20 = {
information_circle: InformationCircleIcon20,
bars_3: Bars3Icon20,
x_mark: XMarkIcon20,
eye: EyeIcon20,
eye_slash: EyeSlashIcon20,
};

export type IconName = keyof typeof icons24 | keyof typeof icons20;
Expand Down
8 changes: 5 additions & 3 deletions frontend2/src/components/elements/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { forwardRef } from "react";
import React, { forwardRef, type ReactElement } from "react";
import FormError from "./FormError";
import FormLabel from "./FormLabel";

Expand All @@ -7,18 +7,19 @@ interface InputProps extends React.ComponentPropsWithoutRef<"input"> {
required?: boolean;
className?: string;
errorMessage?: string;
endButton?: ReactElement;
}

const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{ label, required = false, className = "", errorMessage, ...rest },
{ label, required = false, className = "", errorMessage, endButton, ...rest },
ref,
) {
const invalid = errorMessage !== undefined;
return (
<div className={`relative ${invalid ? "mb-1" : ""} ${className}`}>
<label>
<FormLabel label={label} required={required} />
<div className="relative rounded-md shadow-sm">
<div className="flex w-full flex-row gap-2">
<input
ref={ref}
aria-invalid={errorMessage !== undefined ? "true" : "false"}
Expand All @@ -33,6 +34,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
}`}
{...rest}
/>
{endButton}
</div>
{invalid && <FormError message={errorMessage} />}
</label>
Expand Down
16 changes: 14 additions & 2 deletions frontend2/src/views/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
import Input from "../components/elements/Input";
import Button from "../components/elements/Button";
import { login } from "../api/auth/authApi";
Expand Down Expand Up @@ -38,6 +38,8 @@ const Login: React.FC = () => {
}
};

const [showPassword, setShowPassword] = useState(false);

return (
<div className="flex h-screen flex-col items-center justify-center bg-gradient-to-tr from-cyan-200 to-cyan-700 p-2">
<div className="mb-6 flex flex-1 items-end text-center font-display text-5xl tracking-wide text-white sm:text-6xl">
Expand All @@ -61,7 +63,17 @@ const Login: React.FC = () => {
label="Password"
required
{...register("password", { required: FIELD_REQUIRED_ERROR_MSG })}
type="password"
type={showPassword ? "text" : "password"}
endButton={
<Button
iconName={showPassword ? "eye_slash" : "eye"}
onClick={() => {
setShowPassword((prevShow) => !prevShow);
}}
variant="no-outline"
className="rounded-xl"
/>
}
/>
<Button
label="Log in"
Expand Down
Loading