Next.js でページを見る権限が無いユーザに対しリダイレクトする処理を集約した

Next.js でユーザ認証付きアプリケーションを作っていると「ページを表示したとき、ユーザの状態によってはリダイレクトして、別のページに飛ばしたい」というケースがあります。単純にログインしていないというものから、ユーザに閲覧権限がないなど、見れないなどいくつかケースがあると思います。


例えば、一般アカウントと管理者アカウントが存在するウェブサービスを考えてみます。これらはどちらも「User」というモデルとすることにします。そしてその「User」は内部に「一般アカウントか、管理者アカウントか」という情報を持っていて、特定の url のページは管理者でないと見れないとします。ページを表示するとき、この「User」を API で叩いて初めてどちらかがかわかる、という状態です。


例えば管理者ユーザ用の「admin/」 パス以下を考えます。これは管理者ユーザしかアクセスできないパスであるので、このパス以下の場合は API で 「User」のデータさえ取得できていれば、他のデータを取るまでもなくアクセスできないということにできるはずです。単純にすると、各ページに以下のような処理を書くことになると思います。

const Index: NextPage = () => {
  const user = useGetUser(); // ここで API を叩いて結果を得ている

  useEffect(() => {
    if (!isAdminUser(user) {
      // true ならリダイレクトする
    }

  }, [user]);
     ...
}

管理者しか見れないページにはこれを書いて、そうでないページには書かない。流石にコピペは辛いのでこういう処理を Hooks にして各所にペタペタ貼っていっても良いのですが、貼り忘れる恐れはあります。また今は管理者か否かだけなので良いですが、他にも「User」モデルの様々な状態をみてリダイレクトする処理が増えていくと、各ページに様々な組み合わせの処理が散らばってしまいます。またテストも当然各ページごとに行う必要があります。

自分が関わったプロダクトでは、こういったリダイレクト処理を一箇所の hooks で行い、また「_app.tsx」でのみ行うようにしています。シンプルに管理者アカウントのケースでのみで表すと以下の通りです。

type RedirectCondition = (user: User) => boolean;

const isAdminUser: RedirectCondition = (user) => {
  return // true かどうか判定
};

type RedirectTo = string;

type RedirectRule = {
  from: RegExp;
  to: RedirectTo;
  condition: RedirectCondition;
};

// 登録済みユーザーのリダイレクトルール
const adminUserRules: RedirectRule[] = [
  // 管理画面リンク以下は管理者のみOK
  {
    from: new RegExp(`^admin/`),
    to: () => "/",
    condition: isAdminUser,
  },
  ...
];

const rules: RedirectRule[] = [
  ...adminUserRules,
];

export function useRedirectUser(
  user: User
): void {
  const router = useRouter();
  const { pathname, query } = router;
  const toHref = query.to as string;

  if (!user) {
    return;
  }

  for (const rule of rules) {
    if (rule.from.exec(pathname) && rule.condition(user)) {
      // ここに来たらリダイレクト
      return;
    }
  }
}

すべてのページでこの Hooks 処理が適用されたいので、「_app.tsx」で使います

const App: NextPage = () => {
  const user = useGetUser(); // ここで API を叩いて結果を得ている
  useRedirectUser(user);
  ...
}

すべてのリダイレクト処理が1箇所の hooks に集約されることで、この hooks だけをテストしておけば良くなります。テスタブルなリダイレクトコードを考えていくと、こうやってとある関数に集約されているほうがテストしやすいです。この URL ではこういう条件を満たしていないとリダイレクトする、という処理が一つにまとまっている感じです。