You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js集成Firestore与App Check时触发Missing or insufficient permissions权限错误

Next.js集成Firestore与App Check时触发Missing or insufficient permissions权限错误

我正在为自己开发一个Next.js作品集网站,不需要用户账号,现在卡在了数据处理环节——开启App Check后,我无法正常运行使用Firestore的应用,触发了权限不足的错误。

遇到的错误信息如下:

Erreur API Firestore : [Error [FirebaseError]: Missing or insufficient permissions.] {
  code: 'permission-denied',
  customData: undefined,
  toString: [Function (anonymous)]
}
 GET /api/api-handler?endpoint=cinema 500 in 78ms

相关代码实现

Cinema页面(cinema.js)

这个页面通过getServerSideProps调用内部API获取Firestore数据:

import Nav from '@/components/Nav';
import Head from 'next/head';
import Footer from '@/components/Footer';
import Movie from '@/components/Movie';
import Link from 'next/link';
import React, { useState } from 'react';

export async function getServerSideProps(context) {
  try {
    const protocol = process.env.NODE_ENV === "production" ? "https" : "http";
    const host = context.req.headers.host;
    const response = await fetch(`${protocol}://${host}/api/api-handler?endpoint=cinema`);
    const data = await response.json();

    return {
      props: {
        projects: data,
      },
    };
  } catch (error) {
    console.error("Erreur lors de la récupération des données :", error);
    return {
      props: {
        projects: [],
      },
    };
  }
}

export default function Cinema({ projects }) {
  const [activeFilm, setActiveFilm] = useState(null);
  const [activeItem, setActiveItem] = useState(null);

  return (
    <div className="h-screen text-txt-default font-DM-Sans bg-bg-default bg-right-gradient p-6 flex flex-col justify-between select-none">
      <Head>
        <title>Cinéma</title>
      </Head>

      {/* Barre de navigation */}
      <div className="mb-6">
        <div className="flex justify-between projects-center">
          <div className="flex">
            <Link href="/" className="flex space-x-2 select-none" draggable="false">
              <h6 className="text-txt-default">Jacques</h6>
              <h6 className="text-txt-second">Tellier</h6>
            </Link>
          </div>
          <Nav />
        </div>
        {/* Titre principal */}
        <h2 className="text-accent select-none">CINÉMA</h2>
      </div>

      {/* Contenu principal */}
      <div className="flex flex-col 2xl:flex-row gap-8 2xl:my-20 mx-auto h-[75vh]">
        {/* Bloc des détails */}
        <div className="flex flex-col p-4 2xl:p-6 w-full 2xl:w-80 rounded-lg shadow-muted bg-bg-second order-first 2xl:order-none">
          {activeItem ? (
            <>
              <h4 className="text-txt-default mb-2 2xl:mb-4 text-base 2xl:text-lg">{activeItem.title}</h4>
              <p className="text-txt-muted text-sm 2xl:text-base">{activeItem.description}</p>
              <div className="mt-4 text-xs 2xl:text-sm">
                <p>
                  Année : <span className="text-txt-second">{activeItem.date}</span>
                </p>
                <p>
                  Genre :{" "}
                  <span className="text-txt-second">
                    {activeItem.genre.join(", ")}
                  </span>
                </p>
                <p>
                  Type :{" "}
                  <span className="text-txt-second">
                    {activeItem.type}
                  </span>
                </p>
                <p>
                  Réalisateur(s) : <span className="text-txt-second">{activeItem.director.join(", ")}</span>
                </p>
                <p>
                  Producteur(s) : <span className="text-txt-second">{activeItem.producer.join(", ")}</span>
                </p>
                <p>
                  Distribution :{" "}
                  <span className="text-txt-second">
                    {activeItem.distribution.join(", ")}
                  </span>
                </p>
                <br />
                <p>
                  Mon rôle :{" "}
                  <strong>
                    <span className="text-accent">{activeItem.role}</span>
                  </strong>
                </p>
              </div>
              <button
                onClick={() => setActiveFilm(activeItem)}
                className="mt-4 2xl:mt-10 px-4 2xl:px-6 py-2 2xl:py-3 bg-bg-inverse text-txt-inverse font-bold rounded-full hover:bg-bg-second hover:text-txt-default hover:shadow-inverse transition duration-100 select-none"
                draggable="false"
              >
                Voir +
              </button>
            </>
          ) : (
            <p className="text-txt-muted text-sm 2xl:text-base">Sélectionnez un élément pour afficher les détails.</p>
          )}
        </div>

        {/* Grille des affiches */}
        <div className="grid grid-cols-3 2xl:grid-cols-4 overflow-y-auto scrollbar-hide 2xl:gap-4 order-last 2xl:order-none">
          {projects.length > 0 ? (
            projects.map((item) => (
              <div
                key={item.id}
                className="flex flex-col rounded-lg p-4 hover:scale-105 transition-transform cursor-pointer"
                onClick={() => setActiveItem(item)}
              >
                <img
                  src={item.poster}
                  alt={item.title}
                  className="w-full h-32 2xl:h-64 object-cover rounded-md hover:shadow-inverse"
                />
                <div className="text-center mt-2 2xl:mt-4">
                  <h6 className="font-bold text-sm 2xl:text-base line-clamp-1">{item.title}</h6>
                  <p className="text-xs 2xl:text-sm text-txt-muted">{item.date}</p>
                </div>
              </div>
            ))
          ) : (
            <div className="col-span-full text-center text-lg text-txt-muted">
              Aucun projet pour le moment.
            </div>
          )}
        </div>
      </div>

      {/* Affichage de Movie en position absolue */}
      {activeFilm && (
        <Movie
          film={activeFilm}
          onClose={() => setActiveFilm(null)}
        />
      )}

      {/* Footer */}
      <Footer />
    </div>
  );
}

API处理文件(api-handler.js)

这个文件负责接收请求并从Firestore获取对应集合的数据:

import { collection, getDocs } from "firebase/firestore";
import { db } from "@/config/firebase-config";

export default async function handler(req, res) {
  const { endpoint } = req.query; // Récupère le type de données demandé (ex : "cinema")

  if (req.method === "GET") {
    try {
      console.log("Requête reçue pour endpoint :", endpoint);

      const dataCollection = collection(db, endpoint); // Référencer la collection Firestore
      console.log("\n")
      console.log("Instance Firestore:", dataCollection._firestore);
      console.log("Chemin de la collection :", dataCollection._path.segments);
      console.log("Convertisseur :", dataCollection._converter);
      console.log("\n")
      const snapshot = await getDocs(dataCollection); // Récupérer les documents
      const data = snapshot.docs.map((doc) => ({
        id: doc.id,
        ...doc.data(),
      }));

      console.log("Données récupérées :", data);
      res.status(200).json(data); // Retourne les données récupérées au client
    } catch (error) {
      console.error("Erreur API Firestore :", error);
      res.status(500).json({ error: "Erreur lors de la récupération des données." });
    }
  } else {
    res.setHeader("Allow", ["GET"]);
    res.status(405).end(`Méthode ${req.method} non autorisée`);
  }
}

Firebase配置文件(firebase-config.js)

这里初始化了Firebase应用和App Check:

import { initializeApp, getApps, getApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getStorage } from "firebase/storage";
import { initializeAppCheck, ReCaptchaV3Provider } from "firebase/app-check";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
  databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
  measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
};

const app = !getApps().length ? initializeApp(firebaseConfig) : getApp();
const db = getFirestore(app);
const storage = getStorage(app);

if (typeof window !== "undefined") {
  self.FIREBASE_APPCHECK_DEBUG_TOKEN = true;

  initializeAppCheck(app, {
    provider: new ReCaptchaV3Provider(process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY),
    isTokenAutoRefreshEnabled: true, // Renouvelle automatiquement le jeton
  });
}

备注:内容来源于stack exchange,提问作者Jacques Tellier

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 16:18:06