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
相关产品推荐
相关产品推荐

