Skip to content

Commit

Permalink
feat: upgraded next auth to v5 and now handling guest and protected r…
Browse files Browse the repository at this point in the history
…outes
  • Loading branch information
gustaveWPM committed Apr 21, 2024
1 parent b7c3d3c commit 59e194b
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 98 deletions.
2 changes: 1 addition & 1 deletion .env_example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ NEXT_SITEMAP_SITE_URL="http://localhost:3000"
AUTH_URL="http://localhost:3000"
AUTH_TRUST_HOST="http://localhost:3000"

AUTH_SECRETS="YOUR RANDOM SECRETS, separated using ';;;;;;' (without quotes)" # https://next-auth.js.org/configuration/options#nextauth_secret
AUTH_SECRET="YOUR RANDOM STRING - https://next-auth.js.org/configuration/options#nextauth_secret"

DISCORD_CLIENT_ID="YOUR DISCORD CLIENT ID - https://discord.com/developers/applications"
DISCORD_CLIENT_SECRET="YOUR DISCORD CLIENT SECRET - https://discord.com/developers/applications"
Expand Down
4 changes: 2 additions & 2 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,5 @@ export async function getSession(session: Session, __discordApi: IDiscordApi = d
} satisfies Session;
}

const { handlers, auth } = NextAuth(config);
export { handlers, auth };
const { handlers, signIn, auth } = NextAuth(config);
export { handlers, signIn, auth };
75 changes: 39 additions & 36 deletions src/components/layouts/navbar/NavbarButton.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client';

import type { FunctionComponent, MouseEventHandler, ReactNode } from 'react';
import type { ButtonHTMLAttributes as ReactButtonHTMLAttributes, FunctionComponent, ReactNode } from 'react';
import type { WithClassname, AppPath } from '@rtm/shared-types/Next';
import type { AtomicNavDataEntity } from '@/types/NavData';
import type { AppPath } from '@rtm/shared-types/Next';

import NavbarButtonStyle from '@/components/config/styles/navbar/NavbarButtonStyle';
import { hrefAndPathnameExactMatch, hrefMatchesPathname } from '@/lib/str';
Expand All @@ -15,47 +15,48 @@ import Link from 'next/link';

type OptionalIcon = { icon?: ReactNode };
type OptionalPath = { path?: AppPath };
type OptionalOnClick = { onClick?: MouseEventHandler<HTMLButtonElement> };

type RequiredPath = Required<OptionalPath>;
type RequiredOnClick = Required<OptionalOnClick>;
interface INavbarButtonProps
extends Pick<AtomicNavDataEntity, 'i18nTitle'>,
OptionalPath,
OptionalIcon,
ReactButtonHTMLAttributes<HTMLButtonElement> {}

interface INavbarButtonProps extends Pick<AtomicNavDataEntity, 'i18nTitle'>, OptionalPath, OptionalOnClick, OptionalIcon {}
type NavbarButtonProps = (RequiredOnClick | RequiredPath) & INavbarButtonProps;
type NavbarButtonProps = INavbarButtonProps & OptionalPath;

const { isNotActiveClassList, isActiveClassList } = NavbarButtonStyle;

const ButtonAsIs: FunctionComponent<Pick<NavbarButtonProps, 'i18nTitle' | 'onClick'> & OptionalIcon> = ({ onClick: onClickFun, i18nTitle, icon }) => {
const ButtonAsIs: FunctionComponent<Pick<NavbarButtonProps, 'i18nTitle' | 'onClick' | 'type'> & Partial<WithClassname> & OptionalIcon> = ({
className: classNameValue,
i18nTitle,
onClick,
icon,
type
}) => {
const globalT = getClientSideI18n();

if (onClickFun) {
return icon ? (
<Button className={cn(isNotActiveClassList, 'items-center gap-2 bg-transparent')} onClick={(event) => onClickFun(event)}>
{icon}
{globalT(i18nTitle)}
</Button>
) : (
<Button className={cn(isNotActiveClassList, 'bg-transparent')} onClick={(event) => onClickFun(event)}>
{globalT(i18nTitle)}
</Button>
);
}

return icon ? (
<Button className={cn(isNotActiveClassList, 'bg-transparent')}>
<Button className={cn(isNotActiveClassList, 'bg-transparent', classNameValue)} onClick={onClick} type={type}>
{icon}
{globalT(i18nTitle)}
</Button>
) : (
<Button className={cn(isNotActiveClassList, 'bg-transparent')}>{globalT(i18nTitle)}</Button>
<Button className={cn(isNotActiveClassList, 'bg-transparent', classNameValue)} onClick={onClick} type={type}>
{globalT(i18nTitle)}
</Button>
);
};

const ButtonAsLink: FunctionComponent<Pick<AtomicNavDataEntity, 'i18nTitle' | 'path'> & OptionalIcon> = ({ path: href, i18nTitle, icon }) => {
const ButtonAsLink: FunctionComponent<Pick<AtomicNavDataEntity, 'i18nTitle' | 'path'> & Partial<WithClassname> & OptionalIcon> = ({
className: classNameValue,
path: href,
i18nTitle,
icon
}) => {
const globalT = getClientSideI18n();
const currentPathname = usePathname();
const isActive = hrefMatchesPathname(href, currentPathname);
const className = isActive ? isActiveClassList : isNotActiveClassList;
const className = cn(isActive ? isActiveClassList : isNotActiveClassList, classNameValue);
const target = getLinkTarget(href);
const exactMatch = hrefAndPathnameExactMatch(href, currentPathname);

Expand All @@ -75,18 +76,20 @@ const ButtonAsLink: FunctionComponent<Pick<AtomicNavDataEntity, 'i18nTitle' | 'p
);
};

const NavbarButton: FunctionComponent<NavbarButtonProps> = ({ i18nTitle, onClick, path, icon }) => {
const generateNavbarButtonWithoutIcon: () => ReactNode = () => {
if (onClick) return <ButtonAsIs i18nTitle={i18nTitle} onClick={onClick} />;
else if (path) return <ButtonAsLink i18nTitle={i18nTitle} path={path} />;
return null;
};
const NavbarButton: FunctionComponent<NavbarButtonProps> = ({ i18nTitle, className, onClick, path, icon, type }) => {
const generateNavbarButtonWithoutIcon: () => ReactNode = () =>
path ? (
<ButtonAsLink className={className} i18nTitle={i18nTitle} path={path} />
) : (
<ButtonAsIs className={className} i18nTitle={i18nTitle} onClick={onClick} type={type} />
);

const generateNavbarButtonWithIcon: () => ReactNode = () => {
if (onClick) return <ButtonAsIs i18nTitle={i18nTitle} onClick={onClick} icon={icon} />;
else if (path) return <ButtonAsLink i18nTitle={i18nTitle} icon={icon} path={path} />;
return null;
};
const generateNavbarButtonWithIcon: () => ReactNode = () =>
path ? (
<ButtonAsLink i18nTitle={i18nTitle} className={className} icon={icon} path={path} />
) : (
<ButtonAsIs i18nTitle={i18nTitle} className={className} onClick={onClick} icon={icon} type={type} />
);

return icon ? generateNavbarButtonWithIcon() : generateNavbarButtonWithoutIcon();
};
Expand Down
50 changes: 30 additions & 20 deletions src/components/layouts/navbar/NavbarLoginButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,34 @@ import type { FunctionComponent } from 'react';

import NAVBAR_ICON_STYLE from '@/components/config/styles/navbar/NavbarIconStyle';
import { SignalSlashIcon, KeyIcon } from '@heroicons/react/16/solid';
import { signInProviderActionFlag } from '@/config/authMisc';
import UserImage from '@/components/ui/hoc/UserImage';
import { useSession, signIn } from 'next-auth/react';
import handleSignOut from '@/lib/misc/handleSignOut';
import { useSession, signOut } from 'next-auth/react';
import { signInAction } from '@/lib/authServer';
import { Button } from '@/components/ui/Button';
import { useScopedI18n } from '@/i18n/client';
import { usePathname } from 'next/navigation';
import ROUTES_ROOTS from '##/config/routes';
import { i18ns } from '##/config/i18n';

import NavbarButton from './NavbarButton';

interface NavbarLoginButtonMobileProps extends WithSession {
currentPathname: string;
}
interface NavbarLoginButtonMobileProps extends WithSession {}

interface NavbarLoginButtonProps extends WithIsMobile {}

const { SIZE_PX_VALUE: SIZE } = NAVBAR_ICON_STYLE;
const provider = signInProviderActionFlag;

const NavbarLoginButtonMobile: FunctionComponent<NavbarLoginButtonMobileProps> = ({ currentPathname, session }) => {
const NavbarLoginButtonMobile: FunctionComponent<NavbarLoginButtonMobileProps> = ({ session }) => {
const scopedT = useScopedI18n(i18ns.auth);
const className = 'h-full min-w-0 p-0';

if (session) {
return (
<Button onClick={() => handleSignOut(currentPathname)} withTransparentBackground className={className}>
<Button
onClick={() => {
signOut();
}}
withTransparentBackground
className={className}
>
<UserImage className="absolute rounded-full brightness-75" user={session?.user} height={SIZE} width={SIZE} />
<SignalSlashIcon className="relative shadow-xl" height={SIZE} width={SIZE} />
<span className="sr-only">{scopedT('logout')}</span>
Expand All @@ -41,30 +41,40 @@ const NavbarLoginButtonMobile: FunctionComponent<NavbarLoginButtonMobileProps> =
}

return (
<Button onClick={() => signIn(provider, { callbackUrl: ROUTES_ROOTS.DASHBOARD })} withTransparentBackground className={className}>
<KeyIcon height={SIZE} width={SIZE} />
<span className="sr-only">{scopedT('login')}</span>
</Button>
<form action={signInAction} className="contents">
<Button withTransparentBackground className={className} type="submit">
<KeyIcon height={SIZE} width={SIZE} />
<span className="sr-only">{scopedT('login')}</span>
</Button>
</form>
);
};

const NavbarLoginButton: FunctionComponent<NavbarLoginButtonProps> = ({ isMobile }) => {
const { data: session } = useSession();
const currentPathname = usePathname();
const { auth } = i18ns;

if (isMobile) return <NavbarLoginButtonMobile currentPathname={currentPathname} session={session} />;
if (isMobile) return <NavbarLoginButtonMobile session={session} />;

if (session)
if (session) {
return (
<NavbarButton
icon={<UserImage className="rounded-full" user={session?.user} height={SIZE} width={SIZE} />}
onClick={() => handleSignOut(currentPathname)}
onClick={() => {
signOut();
}}
i18nTitle={`${auth}.logout`}
className="flex gap-2"
type="submit"
/>
);
}

return <NavbarButton onClick={() => signIn(provider, { callbackUrl: ROUTES_ROOTS.DASHBOARD })} i18nTitle={`${auth}.login`} />;
return (
<form action={signInAction} className="contents">
<NavbarButton i18nTitle={`${auth}.login`} type="submit" />
</form>
);
};

export default NavbarLoginButton;
12 changes: 6 additions & 6 deletions src/components/ui/cta/SignupButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
import type { FunctionComponent } from 'react';

import BUTTON_CONFIG from '@/components/config/styles/buttons';
import { signInProviderActionFlag } from '@/config/authMisc';
import { Button } from '@/components/ui/Button';
import { signInAction } from '@/lib/authServer';
import { useScopedI18n } from '@/i18n/client';
import ROUTES_ROOTS from '##/config/routes';
import { signIn } from 'next-auth/react';
import { i18ns } from '##/config/i18n';
import { capitalize } from '@/lib/str';

Expand All @@ -21,9 +19,11 @@ const SignupButton: FunctionComponent<SignUpButtonProps> = () => {
const className = BUTTON_CONFIG.CLASSNAME;

return (
<Button onClick={() => signIn(signInProviderActionFlag, { callbackUrl: ROUTES_ROOTS.DASHBOARD })} className={className} size="lg">
{capitalize(scopedT('signup'))}
</Button>
<form action={signInAction} className="contents">
<Button className={className} type="submit" size="lg">
{capitalize(scopedT('signup'))}
</Button>
</form>
);
};

Expand Down
12 changes: 1 addition & 11 deletions src/config/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,6 @@ import type { NextAuthConfig } from 'next-auth';
import Discord from 'next-auth/providers/discord';
import { getSession } from '@/auth';

// eslint-disable-next-line
const AUTH_SECRETS_SEP = ';;;;;;';

// const secret = process.env.AUTH_SECRETS!.split(AUTH_SECRETS_SEP);
// {ToDo} Find a way to fix this
// See also: https://github.com/nextauthjs/next-auth/issues/10633
const secret = 'huuuummmmm';

const config = {
providers: [
Discord({
Expand All @@ -29,9 +21,7 @@ const config = {
const s = await getSession(session);
return s;
}
},

secret
}
} as const satisfies NextAuthConfig;

export default config;
17 changes: 17 additions & 0 deletions src/lib/authServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* v8 ignore start */
// Stryker disable all

'use server';

import { signInProviderActionFlag } from '@/config/authMisc';
import ROUTES_ROOTS from '##/config/routes';
import { signIn } from '@/auth';

// eslint-disable-next-line require-await
export async function signInAction() {
'use server';
await signIn(signInProviderActionFlag, { callbackUrl: ROUTES_ROOTS.DASHBOARD });
}

// Stryker restore all
/* v8 ignore stop */
22 changes: 0 additions & 22 deletions src/lib/misc/handleSignOut.ts

This file was deleted.

0 comments on commit 59e194b

Please sign in to comment.