Skip to content

Commit

Permalink
Implement saving token and automatically refreshing token
Browse files Browse the repository at this point in the history
  • Loading branch information
NebraskaCoder committed Jun 28, 2024
1 parent 7c075be commit d83d492
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 56 deletions.
14 changes: 11 additions & 3 deletions @types/next-auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ declare module "next-auth" {
* or the second parameter of the `session` callback, when using a database.
*/
interface User {
access_token?: string;
accessToken?: string;
accessTokenExpiration?: number;
refreshTokenExpiration?: number;
apiCookies?: string[];
apiUser?: APIUser;
}

Expand All @@ -17,7 +20,9 @@ declare module "next-auth" {
*/
interface Session {
user: {
access_token?: string;
accessToken?: string;
accessTokenExpiration?: number;
refreshTokenExpiration?: number;
apiUser?: APIUser;
} & DefaultSession["user"];
}
Expand All @@ -26,7 +31,10 @@ declare module "next-auth" {
declare module "next-auth/jwt" {
/** Returned by the `jwt` callback and `getToken`, when using JWT sessions */
interface JWT {
access_token?: string;
accessToken?: string;
accessTokenExpiration?: number;
refreshTokenExpiration?: number;
apiCookies?: string[];
apiUser?: APIUser;
}
}
8 changes: 8 additions & 0 deletions @types/processenv.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
declare namespace NodeJS {
interface ProcessEnv {
NEXTAUTH_SECRET?: string;
NEXTAUTH_URL?: string;
NEXT_PUBLIC_API_BASE_URL?: string;
SERVICE_API_BASE_URL?: string;
}
}
4 changes: 1 addition & 3 deletions app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ export default async function RootLayout({
children: React.ReactNode;
params: { locale: AvailableLanguagesType };
}) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const uncheckedLocale = useLocale();
const locale = checkLanguage(uncheckedLocale);
const locale = checkLanguage(params.locale);
const messages = await getMessages(locale);

// Show a 404 error if the user requests an unknown locale
Expand Down
10 changes: 7 additions & 3 deletions app/[locale]/login/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { classNames } from "@/utils/classNames";
import { signIn } from "next-auth/react";
import { useTranslations } from "next-intl";
import { FormEvent, useRef, useState } from "react";
import { FormEvent, useLayoutEffect, useRef, useState } from "react";
import LoadingDotsSpinner from "./LoadingDotsSpinner";
import { useRouter } from "next/navigation";

Expand Down Expand Up @@ -69,6 +69,12 @@ const Form = ({ callbackUrl }: { callbackUrl?: string }) => {
router.push("/register");
};

useLayoutEffect(() => {
if (emailRef.current) {
emailRef.current.focus();
}
}, []);

return (
<form
className="space-y-6"
Expand All @@ -92,7 +98,6 @@ const Form = ({ callbackUrl }: { callbackUrl?: string }) => {
)}
ref={emailRef}
tabIndex={1}
defaultValue="test@test.com"
/>
{errors.email && <p className="text-red-600">{errors.email}</p>}
</div>
Expand All @@ -116,7 +121,6 @@ const Form = ({ callbackUrl }: { callbackUrl?: string }) => {
)}
ref={passwordRef}
tabIndex={2}
defaultValue="abcabcabc123"
/>
{errors.password && <p className="text-red-600">{errors.password}</p>}
</div>
Expand Down
28 changes: 28 additions & 0 deletions app/api/radio/system/list/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getAuthFetchHeaders } from "@/lib/server/auth";

import { type NextRequest, NextResponse } from "next/server";

export async function GET(req: NextRequest) {
const fetchHeaders = await getAuthFetchHeaders(req);

if (!fetchHeaders) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const systemsRes = await fetch(
`${process.env.SERVICE_API_BASE_URL}/radio/system/list`,
{
method: "GET",
headers: fetchHeaders,
}
);

if (!systemsRes.ok) {
return NextResponse.json(
{ error: systemsRes.statusText },
{ status: systemsRes.status }
);
}

return NextResponse.json(await systemsRes.json());
}
7 changes: 0 additions & 7 deletions components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import { cn } from "@/lib/utils";

import type { DismissableLayerProps } from "@radix-ui/react-dialog";

export type DialogOnEscapeKeyDown = DismissableLayerProps["onEscapeKeyDown"];
export type DialogOnPointerDown = DismissableLayerProps["onPointerDown"];
export type DialogOnInteractOutside =
DismissableLayerProps["onInteractOutside"];

const Dialog = DialogPrimitive.Root;

const DialogTrigger = DialogPrimitive.Trigger;
Expand Down
72 changes: 36 additions & 36 deletions config/nextAuthOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const OPTIONS: AuthOptions = {
CredentialsProvider({
name: "Credentials",
credentials: {
username: {
email: {
label: "Email",
type: "text",
placeholder: "jsmith@gmail.com",
Expand All @@ -24,21 +24,29 @@ export const OPTIONS: AuthOptions = {
return null;
}

// const tokenRes = await axios.post(
// "https://api/auth/token/",
// {
// email: credentials.username,
// password: credentials.password,
// },
// {
// withCredentials: true,
// }
// );
const tokenRes = await fetch(
`${process.env.SERVICE_API_BASE_URL}/auth/token/`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: credentials.email,
password: credentials.password,
}),
}
);

const tokenRes = {
data: {
access_token: "abc123",
},
if (!tokenRes.ok) {
return Promise.resolve(null);
}

const apiToken = (await tokenRes.json()) as {
CSRF_TOKEN: string;
access_token_expiration: string;
refresh_token_expiration: string;
access_token: string;
};

const userRes: { data: APIUser } = {
Expand All @@ -56,33 +64,22 @@ export const OPTIONS: AuthOptions = {
},
};

// if (tokenRes.status === 200) {
// const userRes = await axios.get<APIUser>("https://api/auth/user", {
// headers: {
// Authorization: `Bearer ${tokenRes.data.access_token}`,
// },
// withCredentials: true,
// });

// if (userRes.status === 200) {
// This user object will be returned in the JWT, and will be available in the session
// Include the access_token here so it can be used by your server on future requests

const userData = userRes.data;

const user: User = {
id: userData.id.toString(),
name: `${userData.first_name} ${userData.last_name}`,
email: userData.email,
apiUser: userData,
access_token: tokenRes.data.access_token,
accessToken: apiToken.access_token,
accessTokenExpiration: Date.parse(apiToken.access_token_expiration),
refreshTokenExpiration: Date.parse(
apiToken.refresh_token_expiration
),
apiCookies: tokenRes.headers.getSetCookie(),
};

return Promise.resolve(user);
// }
// }

// return Promise.resolve(null);
} catch (err) {
console.error(err);
return Promise.resolve(null);
Expand All @@ -100,18 +97,21 @@ export const OPTIONS: AuthOptions = {
},
callbacks: {
jwt: async ({ token, user }) => {
// if user is defined, it's the sign in process
if (user) {
token.access_token = user.access_token;
token.accessToken = user.accessToken;
token.accessTokenExpiration = user.accessTokenExpiration;
token.refreshTokenExpiration = user.refreshTokenExpiration;
token.apiCookies = user.apiCookies;
token.apiUser = user.apiUser;
token.name = user.name;
}

return token;
},
session: async ({ session, token }) => {
// add access_token to session
session.user.access_token = token.access_token;
session.user.accessToken = token.accessToken;
session.user.accessTokenExpiration = token.accessTokenExpiration;
session.user.refreshTokenExpiration = token.refreshTokenExpiration;
session.user.apiUser = token.apiUser;
return session;
},
Expand Down
53 changes: 53 additions & 0 deletions lib/server/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { getServerSession as nextAuthGetServerSession } from "next-auth";
import { OPTIONS } from "@/config/nextAuthOptions";
import { getToken } from "next-auth/jwt";
import { parseRefreshToken } from "@/utils/fetchUtils";

import type {
GetServerSidePropsContext,
NextApiRequest,
NextApiResponse,
} from "next";
import type { NextRequest } from "next/server";

export async function getServerSession(
...args:
| [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]]
| [NextApiRequest, NextApiResponse]
| []
) {
return await nextAuthGetServerSession(...args, OPTIONS);
}

export async function getServerJWT(
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest
) {
return await getToken({ req, secret: process.env.JWT_SECRET });
}

export type AuthFetchHeaders =
| { Authorization: string }
| { Authorization: string; Cookie: string }
| undefined;

export async function getAuthFetchHeaders(
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest
): Promise<AuthFetchHeaders> {
const session = await getServerSession();
const token = await getServerJWT(req);

if (!session || !token) {
return undefined;
}

const refreshTokenCookie = parseRefreshToken(token.apiCookies);

return refreshTokenCookie
? {
Authorization: `Bearer ${session.user.accessToken}`,
Cookie: refreshTokenCookie,
}
: {
Authorization: `Bearer ${token.accessToken}`,
};
}
Loading

0 comments on commit d83d492

Please sign in to comment.