Next.js Discord

Discord Forum

get cookies in api client

Unanswered
Evanion posted this in #help-forum
Open in Discord
I'm working on a site that has a separate backend that returns a token, and Next stores that in the users cookies, I'm using mappersmith to create a mapped client of the API. And I have a mappersmith middleware that uses my getToken server action, and includes it in the header of the outgoing request.

but I'm getting errors that I can't use the cookies() object outside of Server actions or Route handlers. The mappersmith client is always called from a server action, and I don't want to have to manually read and set the header each time I make a request.

I'm only using mappersmith to make better DX for data fetching in server actions. So it's never run client side.

2 Replies

Here is some example code:
client.ts:
import forge, { configs } from 'mappersmith';
import { Fetch } from 'mappersmith/gateway/fetch';

import { authMiddleware } from './middleware/auth.middleware';

configs.gateway = Fetch;

export const api = forge({
  clientId: 'backend',
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  host: process.env.API_URL!,
  middleware: [authMiddleware],
  resources: {
    Auth: {
      signUp: { method: 'POST', path: '/authentication/sign-up' },
      signIn: { method: 'POST', path: '/authentication/sign-in' },
      signOut: { method: 'PUT', path: '/authentication/sign-out' },
      refreshTokens: {
        method: 'POST',
        path: '/authentication/refresh-tokens',
        headers: { 'Content-Type': 'application/json' },
      },
      googleAuth: { method: 'POST', path: '/authentication/google' },
    },
    Users: {
      me: { method: 'GET', path: '/users/me' },
      checkUsername: {
        method: 'GET',
        path: '/users/username/{username}/check',
      },
      getUserByUsername: { method: 'GET', path: '/users/username/{username}' },
    },
    Pages: {
      all: { method: 'GET', path: '/pages' },
      byId: { method: 'GET', path: '/pages/{id}' },
      bySlug: { method: 'GET', path: '/pages/slug/{slug}' },
      create: { method: 'POST', path: '/pages' },
      update: { method: 'PATCH', path: '/pages/{id}' },
      remove: { method: 'DELETE', path: '/pages/{id}' },
    },
    Courses: {
      all: { method: 'GET', path: '/courses' },
      byId: { method: 'GET', path: '/courses/{id}' },
      bySlug: { method: 'GET', path: '/courses/slug/{slug}' },
      create: { method: 'POST', path: '/courses' },
      update: { method: 'PATCH', path: '/courses/{id}' },
      remove: { method: 'DELETE', path: '/courses/{id}' },
    },
  },
});

auth.middleware.ts
import { type Middleware } from 'mappersmith';

import { getToken } from '../../actions';

export const authMiddleware: Middleware = () => ({
  async prepareRequest(next) {
    const token = await getToken();
    return next().then((request) => {
      if (!token) return request;

      return request.enhance({
        headers: {
          authorization: `Bearer ${token}`,
        },
      });
    });
  },
});
get-tokens.ts
/* eslint-disable @typescript-eslint/no-non-null-assertion */
'use server';
import { cookies } from 'next/headers';

import { Tokens } from '../types';
import { TokenPayload } from '../types/token-payloads';

const SECOND = 1000;
const MINUTE = 60 * SECOND;

/**
 * Returns the access token to use for API requests against the backend
 * If the token is expired, it will be refreshed
 * If it's unable to refresh the token, it will return undefined
 * @returns accessToken
 */
export async function getToken(): Promise<string | undefined> {
  const cookieStore = cookies();
  const token = cookieStore.get('token');
  const refreshToken = cookieStore.get('refreshToken');

  if (token) {
    // check if token is expiring
    const payload: TokenPayload = JSON.parse(atob(token.value.split('.')[1]));
    const exp = payload.exp;
    const now = Date.now();
    // if token have more than 1 minute left, return it
    if (now - exp > MINUTE) return token.value;
  }

  // if no refresh token, we can't refresh the token
  if (!refreshToken) return;

  try {
    // using fetch to avoid a circular loop with the auth middleware
    const tokens = await fetch(
      `${process.env.API_URL}/authentication/refresh-tokens`,
      {
        method: 'POST',
        body: JSON.stringify({ refreshToken: refreshToken.value }),
      }
    ).then((res) => res.json() as Promise<Tokens>);

    // if unable to refresh token, return undefined
    if (!tokens) return;

    // set the new tokens
    cookieStore.set('token', tokens.accessToken, {
      httpOnly: true,
      maxAge: parseInt(process.env.JWT_ACCESS_TOKEN_TTL!, 10),
    });
    cookieStore.set('refreshToken', tokens.refreshToken, {
      httpOnly: true,
      maxAge: parseInt(process.env.JWT_REFRESH_TOKEN_TTL!, 10),
    });
    cookieStore.set('user', JSON.stringify(tokens.user), {
      httpOnly: false,
    });

    return tokens.accessToken;
  } catch (e) {
    console.error('Failed to refresh tokens', e);
    return undefined;
  }
}

get-current-user.ts
'use server';
import { plainToInstance } from 'class-transformer';

import { User } from '@codesmith/entities';

import { api } from '../api';

export async function getCurrentUser(): Promise<User | undefined> {
  try {
    const user = (await api.Users.me()).data<User>();
    return plainToInstance(User, user);
  } catch (e) {
    return undefined;
  }
}