├── .DS_Store ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── PRIVACY_POLICY.md ├── README.md ├── TERMS_OF_SERVICE.md ├── auth-actions.ts ├── client ├── AuthPages.jsx ├── ProfileButtonStyle.css ├── auth.tsx ├── components.tsx └── getFirebaseErrors.ts ├── dist ├── authpages │ ├── forgot-password │ │ └── page.jsx │ ├── login │ │ └── page.jsx │ └── register │ │ └── page.jsx └── middleware.js ├── firebasenextjs-firebase.ts ├── middleware ├── check-user.js └── firebase-nextjs-middleware.js ├── package-lock.json ├── package.json ├── scripts ├── a_copyComponents.mjs ├── a_setupGcloud.mjs ├── b_setupProject.mjs ├── c_generateServiceAccount.mjs ├── cliUtils.mjs ├── d_setWebApp.mjs ├── e_enableAuth.mjs ├── index.mjs ├── setup.mjs └── utils │ └── getEnv.mjs ├── server ├── auth.ts ├── getToken.d.ts └── getToken.js ├── tsconfig.json └── utilities ├── createSymlink.mjs ├── deleteSymlink.mjs └── preparePackage.mjs /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NirmalScaria/firebase-nextjs/f0c02688357cdda245af50434278f80145f9a9d0/.DS_Store -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [NirmalScaria] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | firebase-service-account.json 3 | firebase-app-config.js 4 | build 5 | .DS_Store 6 | scripts/oauthDetails.mjs -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Nirmal Scaria 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PRIVACY_POLICY.md: -------------------------------------------------------------------------------- 1 | ### Privacy Policy 2 | 3 | #### Introduction 4 | 5 | Welcome to `next-fire-js`! This privacy policy outlines how we handle user data and privacy. We are committed to ensuring your privacy and protecting any information you provide while using our package. 6 | 7 | #### Data Collection 8 | 9 | **No Data Collection**: `next-fire-js` does not collect, store, or process any personal data from users. Our package is designed to facilitate Firebase authentication with Next.js applications without tracking or storing any user information. 10 | 11 | #### Use of OAuth 12 | 13 | **OAuth Authentication**: Our package uses OAuth for authenticating users via Google. This process involves securely communicating with Google servers to authenticate user credentials. 14 | 15 | **No Tracking**: We do not track users during or after the authentication process. The OAuth flow is solely for the purpose of user authentication and session management within your application. 16 | 17 | #### Security 18 | 19 | We prioritize security in our package design to ensure that the authentication process is safe and reliable. However, it is essential for users to follow best practices for securing their Firebase credentials and environment variables when using `next-fire-js`. 20 | 21 | #### Third-Party Services 22 | 23 | `next-fire-js` relies on Firebase for authentication services. Please refer to Firebase's privacy policy for more information on how they handle user data: [Firebase Privacy Policy](https://firebase.google.com/support/privacy). 24 | 25 | #### Changes to This Privacy Policy 26 | 27 | We may update this privacy policy from time to time. Any changes will be posted on this page, and the updated date will be indicated at the top of the policy. 28 | 29 | #### Contact Us 30 | 31 | If you have any questions or concerns about this privacy policy, please contact us at [scaria@scaria.dev]. 32 | 33 | --- 34 | 35 | **Effective Date**: July 4, 2024 36 | 37 | **Last Updated**: July 4, 2024 38 | 39 | By using `next-fire-js`, you acknowledge that you have read and understood this privacy policy. 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![firebase-nextjs](https://github.com/NirmalScaria/nextfirejs/assets/46727865/48e2f4e1-318c-4877-97a8-df1c6e21604c) 3 | 4 | **Effortless Firebase integration for NextJS** 5 | 6 | Demo : [https://firebase-nextjs.scaria.dev](https://firebase-nextjs.scaria.dev) 7 | 8 | 9 | 10 | 11 | # Setup Instructions 12 | 13 | The setup will automatically take care of configurations and credentials. If you would instead prefer to do it all manually, [Click here](#manual-installation). 14 | 15 | Prerequisite: A firebase project. 16 | 17 | ## 1. Install the package 18 | ```bash 19 | npm install firebase-nextjs 20 | ``` 21 | 22 | ## 2. Run the setup script 23 | ```bash 24 | npx firebase-nextjs setup 25 | ``` 26 | 27 | This will 28 | - Prompt you to log in with Google. 29 | - You will be asked to select the firebase project you wish to use. 30 | - You will be asked to choose the service account you wish to use. ("firebase-admin-sdk" is recommended) 31 | - Choose an app you wish to use. (Must be a web app) (If an app doesn't exist, you can create it there.) 32 | - This will generate the necessary authentication credentials, and store it to the project. 33 | - With this, basic setup is complete. 34 | 35 | ## 3. Setup firebase-nextjs Provider 36 | 37 | In the root layout file, (layout.jsx), wrap the whole body in **\** 38 | 39 | ```html 40 | import {FirebaseNextJSProvider} from "firebase-nextjs/client/auth"; 41 | 42 | 43 | 44 | 45 | {children} 46 | 47 | 48 | ``` 49 | 50 | ## 4. Thats it! Run the project. 51 | 52 | ```bash 53 | npm run dev 54 | ``` 55 | 56 | This will require you to sign in to continue. You can use Google Sign In or Email Password Sign In. 57 | 58 | # Access the auth state 59 | 60 | You can access the authentication state, and the user object on client side as well as server side easily. Learn more: 61 | https://firebase-nextjs.scaria.dev/auth 62 | 63 | # Customisation 64 | 65 | firebase-nextjs offers complete customisation of login pages. To learn more, visit: 66 | https://firebase-nextjs.scaria.dev/custom_login 67 | 68 | # Components 69 | 70 | firebase-nextjs comes with many pre built components to help with user authentication and user management. Learn more from: 71 | https://firebase-nextjs.scaria.dev/components 72 | 73 | # Routing 74 | 75 | You can customise the rules for routing, and define which pages are public and which pages are for logged in users. Learn more at: 76 | https://firebase-nextjs.scaria.dev/routing 77 | 78 | # Moving to production 79 | 80 | Before deploying the app to production, there are few configurations and security measures to be done. Read more at: 81 | https://firebase-nextjs.scaria.dev/production 82 | 83 | # For Developers 84 | 85 | If you wish to contribute or make changes to the package, follow the below guide 86 | 87 | ## Step 1: Fork and clone the repo 88 | 89 | Fork the repo to your profile and clone the repo locally. 90 | 91 | ## Step 2: Build the package 92 | 93 | Run the command 94 | 95 | ``` 96 | npm run build 97 | ``` 98 | 99 | This will create a folder 'build' in the directory. It will contain the actual package that you should be using. 100 | 101 | ## Step 3: Link the package to your project 102 | 103 | Create a nextjs project that you wish to test the package with. (Or use your existing NextJS project). Copy the location of the build directory generated from the previous step. 104 | 105 | Run the command 106 | ``` 107 | npx link /path/to/build/folder/of/firebase-nextjs 108 | ``` 109 | 110 | ## Step 4: Use the package 111 | 112 | Now, the package is linked to your project, and it is exactly as if you have installed the package. **Do not add the package name to package.json.** 113 | 114 | To use the package in your project, 115 | You can import the files just like you would form an installed package. 116 | 117 | Example: 118 | ``` 119 | import { ProfileButton } from "firebase-nextjs/client/components"; 120 | ``` 121 | 122 | To use the npx script 123 | You can run the npx script as usual, and the code in your build folder will be used. 124 | ``` 125 | npx firebase-nextjs getenv 126 | ``` 127 | 128 | ## Step 5: Modifications 129 | 130 | After you make any modification to the source code, run 131 | ``` 132 | npm run build 133 | ``` 134 | Thats it and your project will be using the newly built package. 135 | 136 | ## NOTE: Installing other packages 137 | 138 | Whenever you use the command `npm install packagename` to install any package, the npx link will be removed. 139 | Re run the npx command to link to the build directory again. 140 | 141 | ## Contributing 142 | 143 | If you have done any modification that might be useful to others, you are welcome to create a pull request and become a contributor. 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /TERMS_OF_SERVICE.md: -------------------------------------------------------------------------------- 1 | ### Terms of Service 2 | 3 | #### Introduction 4 | 5 | Welcome to `next-fire-js`. By using our package, you agree to comply with and be bound by the following terms and conditions. Please read these terms carefully before using `next-fire-js`. 6 | 7 | #### Use of Package 8 | 9 | `next-fire-js` is provided as an open-source tool to facilitate the integration of Firebase Authentication with Next.js applications. By using this package, you agree to adhere to all applicable laws and regulations. 10 | 11 | #### No Guarantees 12 | 13 | `next-fire-js` is provided "as is" without any guarantees or warranties of any kind. We do not guarantee that the package will meet your requirements, be error-free, or operate without interruptions. 14 | 15 | #### Disclaimer of Warranties 16 | 17 | To the fullest extent permitted by applicable law, `next-fire-js` disclaims all warranties, express or implied, including, but not limited to, implied warranties of merchantability, fitness for a particular purpose, and non-infringement. We do not warrant that the package will be free from defects or that any defects will be corrected. 18 | 19 | #### Limitation of Liability 20 | 21 | In no event shall `next-fire-js` or its contributors be liable for any direct, indirect, incidental, special, consequential, or punitive damages arising out of your use of, or inability to use, the package, even if advised of the possibility of such damages. 22 | 23 | #### Indemnification 24 | 25 | You agree to indemnify and hold harmless `next-fire-js`, its contributors, and its affiliates from and against any and all claims, liabilities, damages, losses, or expenses arising out of or in any way connected with your use of the package. 26 | 27 | #### Changes to Terms 28 | 29 | We reserve the right to modify these terms of service at any time. Any changes will be posted on this page, and the updated date will be indicated at the top of the terms. Your continued use of `next-fire-js` after any changes signifies your acceptance of the updated terms. 30 | 31 | #### Governing Law 32 | 33 | These terms shall be governed by and construed in accordance with the laws of India, without regard to its conflict of law principles. 34 | 35 | #### Contact Us 36 | 37 | If you have any questions or concerns about these terms of service, please contact us at [scaria@scaria.dev]. 38 | 39 | --- 40 | 41 | **Effective Date**: July 4, 2024 42 | 43 | **Last Updated**: July 4, 2024 44 | 45 | By using `next-fire-js`, you acknowledge that you have read and understood these terms of service and agree to be bound by them. 46 | -------------------------------------------------------------------------------- /auth-actions.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "./firebasenextjs-firebase"; 2 | import { 3 | createUserWithEmailAndPassword, 4 | signInWithEmailAndPassword, 5 | sendPasswordResetEmail, 6 | sendEmailVerification, 7 | updatePassword, 8 | signInWithPopup, 9 | GoogleAuthProvider, 10 | } from "firebase/auth"; 11 | 12 | export const doCreateUserWithEmailAndPassword = async (email: string, password: string) => { 13 | return createUserWithEmailAndPassword(auth, email, password); 14 | }; 15 | 16 | export const doSignInWithEmailAndPassword = (email: string, password: string) => { 17 | return signInWithEmailAndPassword(auth, email, password); 18 | }; 19 | 20 | export const doSignInWithGoogle = async () => { 21 | const provider = new GoogleAuthProvider(); 22 | const result = await signInWithPopup(auth, provider); 23 | return result; 24 | }; 25 | 26 | export async function doSignOut({ persist }: { persist: boolean }) { 27 | await auth.signOut(); 28 | if (persist) { 29 | window.location.reload(); 30 | } 31 | else { 32 | window.location.href = "/"; 33 | } 34 | } 35 | 36 | export const doPasswordReset = (email: string) => { 37 | return sendPasswordResetEmail(auth, email); 38 | }; 39 | 40 | export const doPasswordChange = (password: string) => { 41 | return updatePassword(auth.currentUser!, password); 42 | }; 43 | 44 | export const doSendEmailVerification = () => { 45 | return sendEmailVerification(auth.currentUser!, { 46 | url: `${window.location.origin}/home`, 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /client/AuthPages.jsx: -------------------------------------------------------------------------------- 1 | import ForgotPasswordPage from "/components/firebase-nextjs/ForgotPasswordPage"; 2 | import LoginPage from "/components/firebase-nextjs/LoginPage"; 3 | import RegisterPage from "/components/firebase-nextjs/RegisterPage"; 4 | export default async function AuthPages({ searchParams }) { 5 | const path = searchParams.path; 6 | if (path == "/login") { 7 | return 8 | } 9 | if (path == "/register") { 10 | return 11 | } 12 | if (path == "/forgot-password") { 13 | return 14 | } 15 | } -------------------------------------------------------------------------------- /client/ProfileButtonStyle.css: -------------------------------------------------------------------------------- 1 | .profileLogout { 2 | padding-left: 13px; 3 | padding-right: 13px; 4 | padding-top: 13px; 5 | padding-bottom: 10px; 6 | font-size: 14px; 7 | font-weight: 400; 8 | color: #000000aa; 9 | cursor: pointer; 10 | display: flex; 11 | flex-direction: row; 12 | gap: 5px; 13 | justify-items: center; 14 | background-color: transparent; 15 | transition: all 0.3s; 16 | } 17 | 18 | .profileLogout:hover { 19 | color: #000000; 20 | background-color: #00000009; 21 | } -------------------------------------------------------------------------------- /client/auth.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useContext, useState, useEffect } from "react"; 3 | import { auth } from "../firebasenextjs-firebase"; 4 | import { onAuthStateChanged } from "firebase/auth"; 5 | import { User } from "firebase/auth"; 6 | import { getToken } from "../server/getToken"; 7 | 8 | type FirebaseNextJSContextType = { 9 | userLoggedIn: boolean; 10 | isEmailUser: boolean; 11 | currentUser: User | null; 12 | }; 13 | 14 | const FirebaseNextJSContext = React.createContext({ 15 | userLoggedIn: false, 16 | isEmailUser: false, 17 | currentUser: null, 18 | } as FirebaseNextJSContextType); 19 | 20 | export function getUserCS() { 21 | return useContext(FirebaseNextJSContext); 22 | } 23 | 24 | export function FirebaseNextJSProvider({ children }: { children: React.ReactNode }) { 25 | const [currentUser, setCurrentUser] = useState(null); 26 | const [userLoggedIn, setUserLoggedIn] = useState(false); 27 | const [isEmailUser, setIsEmailUser] = useState(false); 28 | const [loading, setLoading] = useState(true); 29 | 30 | useEffect(() => { 31 | const unsubscribe = onAuthStateChanged(auth, initializeUser); 32 | return unsubscribe; 33 | }, []); 34 | 35 | async function initializeUser(user: User | null) { 36 | if (user) { 37 | 38 | 39 | const isEmail = user.providerData.some( 40 | (provider) => provider.providerId === "password" 41 | ); 42 | setIsEmailUser(isEmail); 43 | 44 | user.getIdToken(true).then(async function (idToken) { 45 | const sessionToken = await getToken({ idToken }); 46 | document.cookie = `firebase_nextjs_token=${sessionToken}; expires=${new Date(Date.now() + 3600 * 1000 * 24 * 14).toUTCString()}; path=/;`; 47 | }).catch(async function (error) { 48 | console.error(error) 49 | console.error("FAILED TO GET ID TOKEN") 50 | // document.cookie = "firebase_nextjs_token="; 51 | // await auth.signOut(); 52 | // window.location.reload(); 53 | }); 54 | 55 | setCurrentUser({ ...user }); 56 | setUserLoggedIn(true); 57 | } else { 58 | setCurrentUser(null); 59 | setUserLoggedIn(false); 60 | document.cookie = "firebase_nextjs_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/;"; 61 | } 62 | 63 | setLoading(false); 64 | } 65 | 66 | const value = { 67 | userLoggedIn, 68 | isEmailUser, 69 | currentUser 70 | }; 71 | 72 | return ( 73 | 74 | {loading ? : children} 75 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /client/components.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import "./ProfileButtonStyle.css"; 3 | import { doSignOut } from "../auth-actions"; 4 | import { GoogleAuthProvider, GithubAuthProvider, signInWithPopup, signInWithEmailAndPassword, createUserWithEmailAndPassword, User, signInWithRedirect } from "firebase/auth"; 5 | import { auth } from "../firebasenextjs-firebase"; 6 | import { getUserCS } from "./auth"; 7 | import { decodeFirebaseError } from "./getFirebaseErrors"; 8 | import React, { useState } from "react"; 9 | import { Popover, PopoverPosition } from "react-tiny-popover"; 10 | 11 | /** 12 | * 13 | * @param children - The component that should trigger the sign out. 14 | * @param persistRoute [Optional] - If true, the the user will be redirected to the same page after logging back in. Default is false. 15 | * @returns 16 | */ 17 | export function LogoutButton({ children, persistRoute }: { children: React.ReactNode, persistRoute?: boolean }) { 18 | function signOutAction() { 19 | doSignOut({ persist: persistRoute ?? false }) 20 | } 21 | return
{children}
22 | } 23 | 24 | export function LoggedInContent({ children }: { children: React.ReactNode }) { 25 | const { currentUser } = getUserCS(); 26 | return currentUser ? <>{children} : null; 27 | } 28 | 29 | export function LoggedOutContent({ children }: { children: React.ReactNode }) { 30 | const { currentUser } = getUserCS(); 31 | return currentUser ? null : <>{children}; 32 | } 33 | 34 | 35 | /** 36 | * 37 | * !! IMPORTANT !! 38 | * If using redirect method, additional configuration is required. Read examples below for more details. 39 | * @param children - The component that should trigger the sign in with Google. If not provided, a default button will be shown. 40 | * @param className - The class name of the button 41 | * @param method - The method of sign in. Can be "popup" or "redirect". Default is "popup". Read examples for more details. 42 | * 43 | * @returns A component that triggers the sign in with Google. 44 | * 45 | * @example 46 | * // 1. Using default UI and popup 47 | * // ------------------------------------- 48 | * 49 | * 50 | * @example 51 | * // 2. Custom UI and popup 52 | * // ------------------------------------- 53 | * 54 | * 55 | * 56 | * 57 | * @example 58 | * // 3. Using redirect 59 | * // ------------------------------------- 60 | * 61 | * 62 | * 63 | * // SETUP (Required only for redirect method): 64 | * // 1. Go to Firebase Console > Authentication > Settings > Authorized Domains 65 | * // 2. Add your domain. (Add "localhost" to test locally). 66 | * // 3. Open "firebase-app-config.json" and copy the authDomain value. (Ex: "your-app.firebaseapp.com") 67 | * // 4. Edit nextjs.config.mjs and add the rewrite rules as shown below. 68 | * 69 | * // nextjs.config.mjs 70 | * const nextConfig = { 71 | * async rewrites() { 72 | * return [ 73 | * { 74 | * source: '/__/auth/:path*', 75 | * destination: 'https://your-app.firebaseapp.com/__/auth/:path*', 76 | * }, 77 | * ]; 78 | * } 79 | *}; 80 | * 81 | * export default nextConfig; 82 | * 83 | * // 5. Edit firebase-app-config.json and change the authDomain value to your app's domain. 84 | * // Example: "authDomain": "your-website.com" 85 | * // If locally: "authDomain": "localhost:3000" 86 | * 87 | * // 6. Enforce https. (Required) 88 | * // For localhost, this can be done by changing "next dev" in package.json to "next dev --experimental-https" 89 | * 90 | * // If you come across any issue, please file the issue on GitHub. 91 | * @link https://github.com/NirmalScaria/firebase-nextjs 92 | * 93 | */ 94 | export function GoogleSignInButton({ children, className, method = "popup" }: { children?: React.ReactNode, className?: string, method?: "popup" | "redirect" }) { 95 | const doSignInWithGoogle = async () => { 96 | const provider = new GoogleAuthProvider(); 97 | const resp = await signInWithPopup(auth, provider); 98 | if (resp) { 99 | setTimeout(() => { 100 | window.location.reload(); 101 | }, 2500); 102 | } 103 | }; 104 | 105 | const doSignInWithGoogleRedirect = async () => { 106 | const provider = new GoogleAuthProvider(); 107 | signInWithRedirect(auth, provider); 108 | }; 109 | 110 | function GoogleLogo({ height = 24, width = 24, ...props }) { 111 | return 112 | } 113 | 114 | return
115 | { 116 | children ?? 117 | 121 | } 122 |
123 | } 124 | 125 | export function GithubSignInButton({ children, className }: { children?: React.ReactNode, className?: string }) { 126 | const doSignInWithGithub = async () => { 127 | const provider = new GithubAuthProvider(); 128 | console.log("Provider : ", provider) 129 | const resp = await signInWithPopup(auth, provider); 130 | console.log("Resp : ", resp) 131 | if (resp) { 132 | setTimeout(() => { 133 | window.location.reload(); 134 | }, 2500); 135 | } 136 | }; 137 | 138 | function GithubLogo({ height = 24, width = 24, ...props }) { 139 | return 140 | 141 | 142 | } 143 | 144 | return
145 | { 146 | children ?? 147 | 151 | } 152 |
153 | } 154 | 155 | export function EmailSignInButton({ children, email, password, setErrorMessage, className, setLoading }: { 156 | children: React.ReactNode, 157 | email: string, 158 | password: string, 159 | setErrorMessage: (msg: string) => any, 160 | className?: string, 161 | setLoading?: (loading: boolean) => any 162 | }) { 163 | async function doSignInWithEmailAndPassword() { 164 | if (setLoading) setLoading(true); 165 | try { 166 | const userCredential = await signInWithEmailAndPassword(auth, email, password) 167 | if (userCredential) { 168 | setTimeout(() => { 169 | window.location.reload(); 170 | }, 2500); 171 | } 172 | if (setLoading) setLoading(false) 173 | } 174 | catch (error: any) { 175 | setErrorMessage(decodeFirebaseError({ errorCode: error.code })) 176 | if (setLoading) setLoading(false) 177 | } 178 | } 179 | return
{children}
180 | } 181 | 182 | export function EmailSignUpButton({ children, email, password, setErrorMessage, className, setLoading }: { 183 | children: React.ReactNode, 184 | email: string, 185 | password: string, 186 | setErrorMessage: (msg: string) => void, 187 | className?: string, 188 | setLoading?: (loading: boolean) => void 189 | 190 | }) { 191 | async function doCreateUserWithEmailAndPassword() { 192 | if (setLoading) setLoading(true); 193 | try { 194 | const userCredential = await createUserWithEmailAndPassword(auth, email, password) 195 | if (userCredential) { 196 | setTimeout(() => { 197 | window.location.reload(); 198 | }, 2500); 199 | } 200 | if (setLoading) setLoading(false) 201 | } 202 | catch (error: any) { 203 | setErrorMessage(decodeFirebaseError({ errorCode: error.code })) 204 | if (setLoading) setLoading(false) 205 | } 206 | } 207 | return
{children}
208 | } 209 | 210 | export function ProfileButton({ size = 30, positions = ["bottom", "left", "right", "top"] }: { size?: number, positions?: PopoverPosition[] }) { 211 | const [isPopoverOpen, setIsPopoverOpen] = useState(false); 212 | const { currentUser } = getUserCS(); 213 | 214 | if (!currentUser) return
; 215 | 216 | return setIsPopoverOpen(false)} content={ 217 | } containerStyle={{ zIndex: "999" }}> 218 |
setIsPopoverOpen(!isPopoverOpen)}> 219 | 220 |
221 |
222 | } 223 | 224 | function LogoutLogo({ height = 20, width = 20, ...props }) { 225 | return 226 | } 227 | 228 | function ProfileButtonTrigger({ user, size }: { user: User | null, size: number }) { 229 | const imageUrl = user?.photoURL ?? "https://ui-avatars.com/api/?background=0D8ABC&color=fff&name=" + (user?.displayName ?? user?.email); 230 | return ( 231 | profile 232 | ); 233 | }; 234 | 235 | function ProfilePopup({ user }: { user: User | null }) { 236 | 237 | const imageUrl = user?.photoURL ?? "https://ui-avatars.com/api/?background=0D8ABC&color=fff&name=" + (user?.displayName ?? user?.email); 238 | 239 | const popupStyle: React.CSSProperties = { 240 | width: "calc(-40px + min(100vw, 370px))", 241 | backgroundColor: '#fff', 242 | border: '1px solid #00000022', 243 | borderRadius: 8, 244 | color: "#000", 245 | padding: 0, 246 | paddingTop: 10, 247 | margin: 10, 248 | }; 249 | 250 | const profilePopupImageStyle: React.CSSProperties = { 251 | borderRadius: 9999, 252 | height: 30, 253 | width: 30, 254 | margin: 5, 255 | marginLeft: 13, 256 | marginTop: 8, 257 | }; 258 | 259 | return
260 |
261 | profile 262 |
263 | {user?.displayName &&
{user?.displayName}
} 270 |
{user?.email}
271 |
272 |
273 |
274 | 275 |
276 | 277 | Log Out 278 |
279 |
280 |
281 | }; -------------------------------------------------------------------------------- /client/getFirebaseErrors.ts: -------------------------------------------------------------------------------- 1 | export function decodeFirebaseError({ errorCode }: {errorCode: string}) { 2 | if (errorCode == "auth/invalid-email") return "Invalid Email Address" 3 | if (errorCode == "auth/user-disabled") return "User Account Disabled" 4 | if (errorCode == "auth/user-not-found") return "User Account Not Found" 5 | if (errorCode == "auth/wrong-password") return "Incorrect Password" 6 | if (errorCode == "auth/missing-password") return "Please Enter a Password" 7 | if (errorCode == "auth/email-already-in-use") return "Email already in use. Please login with Google" 8 | if (errorCode == "auth/weak-password") return "Weak Password" 9 | if (errorCode == "auth/operation-not-allowed") return "Operation Not Allowed" 10 | if (errorCode == "auth/credential-already-in-use") return "Credential Already in Use" 11 | if (errorCode == "auth/account-exists-with-different-credential") return "Account Exists with Different Credential" 12 | if (errorCode == "auth/invalid-credential") return "Invalid Credential" 13 | if (errorCode == "auth/invalid-verification-code") return "Invalid Verification Code" 14 | return "Unable to login. Please try again." 15 | } -------------------------------------------------------------------------------- /dist/authpages/forgot-password/page.jsx: -------------------------------------------------------------------------------- 1 | export default async function ForgotPasswordPage() { 2 | return
I tend to forget
3 | } -------------------------------------------------------------------------------- /dist/authpages/login/page.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { getUserCS } from "firebase-nextjs/client/auth" 3 | import { GoogleSignInButton, EmailSignInButton } from "firebase-nextjs/client/components"; 4 | import Link from "next/link"; 5 | import { useState } from "react"; 6 | 7 | export default function LoginPage() { 8 | 9 | const { currentUser } = getUserCS(); 10 | 11 | const [email, setEmail] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | const [errorMessage, setErrorMessage] = useState(""); 14 | 15 | const [loading, setLoading] = useState(false); 16 | 17 | function handleChange(e) { 18 | if (e.target.type === "email") setEmail(e.target.value); 19 | if (e.target.type === "password") setPassword(e.target.value); 20 | setErrorMessage("") 21 | } 22 | 23 | return
24 |
25 |
26 | {!currentUser ? <> 27 |

Sign in to Your App

28 |

Sign in to access your Awesome New Tool

29 | 30 | 31 | { 32 | errorMessage != "" && 33 | 34 | {errorMessage} 35 | 36 | } 37 | 38 | 41 | 42 |
43 |
44 |

or

45 |
46 |
47 | 48 | 52 | 53 | : } 54 |
55 |
New here? 56 | Sign Up 57 |
58 |
59 |
60 | } 61 | 62 | function GoogleLogo({ height = 24, width = 24, ...props }) { 63 | return 64 | } 65 | 66 | function Spinner({ className }) { 67 | return
68 |
69 | } -------------------------------------------------------------------------------- /dist/authpages/register/page.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { getUserCS } from "firebase-nextjs/client/auth" 3 | import { GoogleSignInButton, EmailSignUpButton } from "firebase-nextjs/client/components"; 4 | import Link from "next/link"; 5 | import { useState } from "react"; 6 | 7 | export default function LoginPage() { 8 | 9 | const { currentUser } = getUserCS(); 10 | 11 | const [email, setEmail] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | const [errorMessage, setErrorMessage] = useState(""); 14 | 15 | const [loading, setLoading] = useState(false); 16 | 17 | function handleChange(e) { 18 | if (e.target.type === "email") setEmail(e.target.value); 19 | if (e.target.type === "password") setPassword(e.target.value); 20 | setErrorMessage("") 21 | } 22 | 23 | return
24 |
25 |
26 | {!currentUser ? <> 27 |

Sign Up With Your App

28 |

Create an account to access your awesome tool!

29 | 30 | 31 | { 32 | errorMessage != "" && 33 | 34 | {errorMessage} 35 | 36 | } 37 | 38 | 41 | 42 |
43 |
44 |

or

45 |
46 |
47 | 48 | 52 | 53 | : } 54 |
55 |
Existing user? 56 | Sign in 57 |
58 |
59 |
60 | } 61 | 62 | function GoogleLogo({ height = 24, width = 24, ...props }) { 63 | return 64 | } 65 | 66 | function Spinner({ className }) { 67 | return
68 |
69 | } -------------------------------------------------------------------------------- /dist/middleware.js: -------------------------------------------------------------------------------- 1 | import FirebaseNextJSMiddleware from "firebase-nextjs/middleware/firebase-nextjs-middleware"; 2 | 3 | const options = { 4 | allowRule: "^\/_next\/|\/__\/auth\/.*" // Allow paths under /_next/ and /__/auth/ (for firebase auth) publically. 5 | } 6 | 7 | export default function middleware(req) { 8 | return FirebaseNextJSMiddleware({ req, options }); 9 | } -------------------------------------------------------------------------------- /firebasenextjs-firebase.ts: -------------------------------------------------------------------------------- 1 | import { getApps, initializeApp } from "firebase/app"; 2 | import { getAuth } from "firebase/auth"; 3 | 4 | //@ts-ignore 5 | import { firebaseConfig } from "/firebase-app-config"; 6 | 7 | const app = initializeApp(firebaseConfig); 8 | const auth = getAuth(app) 9 | 10 | export function getApp() { 11 | if (getApps().length === 0) { 12 | return initializeApp(firebaseConfig); 13 | } 14 | else { 15 | return getApps()[0]; 16 | } 17 | } 18 | 19 | export { app, auth }; -------------------------------------------------------------------------------- /middleware/check-user.js: -------------------------------------------------------------------------------- 1 | import { cookies } from "next/headers"; 2 | import { jwtDecode } from "jwt-decode"; 3 | import { firebaseConfig } from "/firebase-app-config"; 4 | 5 | const google_cert_urls = [ 6 | "https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys", 7 | "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"] 8 | export default async function checkUser() { 9 | const cookieStore = cookies(); 10 | const token = cookieStore.get("firebase_nextjs_token")?.value; 11 | if (!token) { 12 | return false; 13 | } 14 | var headers; 15 | var body 16 | try { 17 | headers = jwtDecode(token, { header: true }); 18 | body = jwtDecode(token); 19 | } 20 | catch (e) { 21 | console.warn("INVALID JSON") 22 | return false; 23 | } 24 | const keys = await getGoogleKeys() 25 | if (!(keys.includes(headers.kid))) { 26 | console.warn("INVALID JSON : KID NOT MATCH") 27 | return false; 28 | } 29 | const current_time = Math.floor(Date.now() / 1000); 30 | if (body.exp < current_time) { 31 | console.warn("TOKEN EXPIRED") 32 | return false; 33 | } 34 | if (body.iat > current_time) { 35 | console.warn("TOKEN ISSUED IN FUTURE") 36 | return false; 37 | } 38 | const projectId = firebaseConfig.projectId; 39 | if (body.aud != projectId) { 40 | console.warn("INVALID AUDIENCE") 41 | return false; 42 | } 43 | if (body.iss != "https://session.firebase.google.com/" + projectId && body.iss != "https://securetoken.google.com/" + projectId) { 44 | console.warn("INVALID ISSUER") 45 | return false; 46 | } 47 | if (body.sub == undefined) { 48 | console.warn("INVALID SUBJECT") 49 | return false; 50 | } 51 | if (body.auth_time > current_time) { 52 | console.warn("INVALID AUTH TIME") 53 | return false; 54 | } 55 | return true 56 | 57 | } 58 | 59 | async function getGoogleKeys() { 60 | const responses = google_cert_urls.map(async (url) => { 61 | const res = await fetch(url); 62 | return res.json(); 63 | }); 64 | const body = await Promise.all(responses); 65 | return (Object.keys(body[0]).concat(Object.keys(body[1]))); 66 | } -------------------------------------------------------------------------------- /middleware/firebase-nextjs-middleware.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import checkUser from './check-user'; 3 | 4 | const AUTH_PATHS = [ 5 | "/login", 6 | "/register", 7 | "/forgot-password" 8 | ] 9 | 10 | export default async function FirebaseNextJSMiddleware({ req, middleware = undefined, options = {} }) { 11 | const path = req.nextUrl.pathname; 12 | const loggedIn = await checkUser(); 13 | middleware = middleware ?? ((req) => { return NextResponse.next() }); 14 | 15 | // If user is already logged in, and tries an auth page 16 | // Redirect to the target page 17 | if (loggedIn && AUTH_PATHS.includes(path)) { 18 | const target = req.nextUrl.searchParams.get('target') ?? "/"; 19 | return NextResponse.redirect(new URL(target, req.nextUrl)); 20 | } 21 | 22 | // Requesting an auth page. 23 | // These are special routes handled by FirebaseNextJS auth. 24 | if (AUTH_PATHS.includes(path)) { 25 | return NextResponse.next() 26 | } 27 | 28 | // If a regex rule is defined in allowRule, allow the path if it matches 29 | // Every other form of rule specification is ignored. 30 | if (options.allowRule != undefined) { 31 | const rule = new RegExp(options.allowRule) 32 | if (rule.test(path)) { 33 | return middleware(req) 34 | } 35 | else { 36 | if (loggedIn) { 37 | return middleware(req) 38 | } 39 | return NextResponse.redirect(new URL('/login?target=' + path, req.nextUrl)); 40 | } 41 | } 42 | 43 | if (options.gateMode == "allowByDefault") { 44 | // Routes will be allowed by default 45 | // Routes in privatePaths will be denied for unauthenticated users 46 | if (options.privatePaths.includes(path) && !loggedIn) { 47 | return NextResponse.redirect(new URL('/login?target=' + path, req.nextUrl)); 48 | } 49 | return middleware(req) 50 | } 51 | // Routes will be denied by default 52 | // Routes in publicPaths will be allowed for unauthenticated users 53 | if (options.publicPaths.includes(path) || loggedIn) { 54 | return middleware(req) 55 | } 56 | return NextResponse.redirect(new URL('/login?target=' + path, req.nextUrl)); 57 | } 58 | 59 | export const config = { 60 | matcher: '/login', 61 | }; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase-nextjs", 3 | "version": "1.7.5", 4 | "description": "Connect Next.js with Firebase Authentication.", 5 | "private": true, 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "clean": "rimraf build", 9 | "copy-files": "npm run clean && copyfiles '**/*' build/ -e 'node_modules/**/*' -e 'utilities/**/*' -e 'firebase-app-config.js' -e 'firebase-service-account.json'", 10 | "prepare-package": "npm run create-symlink && node utilities/preparePackage.mjs", 11 | "create-symlink": "node utilities/createSymlink.mjs", 12 | "delete-symlink": "node utilities/deleteSymlink.mjs", 13 | "build": "npm run copy-files && npm run prepare-package && cd build && tsc && cd .. && npm run delete-symlink", 14 | "autopublish": "npm i && npm run build && cd build && npm publish" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/NirmalScaria/firebase-nextjs.git" 19 | }, 20 | "bin": "./scripts/index.mjs", 21 | "keywords": [ 22 | "nextjs", 23 | "firebase", 24 | "authentication", 25 | "integration", 26 | "nextfire" 27 | ], 28 | "author": "Nirmal Scaria ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/NirmalScaria/firebase-nextjs/issues" 32 | }, 33 | "homepage": "https://firebase-nextjs.scaria.dev", 34 | "dependencies": { 35 | "chalk": "^5.3.0", 36 | "commander": "^12.1.0", 37 | "express": "^4.19.2", 38 | "firebase-admin": "^10.3.0", 39 | "googleapis": "^140.0.0", 40 | "inquirer": "^9.2.23", 41 | "jwt-decode": "~4.0.0", 42 | "next": "^13.0.0", 43 | "open": "^10.1.0", 44 | "react": "*", 45 | "fast-crc32c": "*", 46 | "react-tiny-popover": "^8.0.4" 47 | }, 48 | "devDependencies": { 49 | "@types/react": "*", 50 | "copyfiles": "^2.4.1", 51 | "firebase": "^10.0.0", 52 | "rimraf": "^5.0.7", 53 | "typescript": "^4.4.4" 54 | }, 55 | "peerDependencies": { 56 | "next": "^13.0.0", 57 | "firebase": "^10.0.0", 58 | "react": "*" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /scripts/a_copyComponents.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | 4 | const copyFiles = (source, target) => { 5 | const files = fs.readdirSync(source); 6 | files.forEach(file => { 7 | const filePath = path.join(source, file); 8 | const targetPath = path.join(target, file); 9 | if (fs.lstatSync(filePath).isDirectory()) { 10 | if (!fs.existsSync(targetPath)) fs.mkdirSync(targetPath); 11 | copyFiles(filePath, targetPath); 12 | } else { 13 | fs.copyFileSync(filePath, targetPath); 14 | } 15 | }); 16 | } 17 | 18 | export async function copyComponents() { 19 | var isUsingSrc = false; 20 | 21 | // COPY THE COMPONENTS 22 | 23 | const cwd = process.cwd(); 24 | var source = path.join(cwd, 'node_modules/firebase-nextjs/dist/components/firebase-nextjs'); 25 | var target = path.join(cwd, ''); 26 | 27 | // If @/src exists, copy to @/src/components 28 | const srcTarget = path.join(target, 'src'); 29 | if (fs.existsSync(srcTarget)) { 30 | target = srcTarget; 31 | isUsingSrc = true; 32 | } 33 | 34 | var routerPath = path.join(cwd, ''); 35 | if (fs.existsSync(path.join(routerPath, 'src'))) routerPath = path.join(routerPath, 'src'); 36 | 37 | const appRouterPath = path.join(routerPath, 'app'); 38 | const pagesRouterPath = path.join(routerPath, 'pages'); 39 | 40 | if (fs.existsSync(appRouterPath)) { 41 | console.log("App router detected"); 42 | const nextFireTarget = path.join(appRouterPath, '(authpages)'); 43 | const nextFireSource = path.join(cwd, 'node_modules/firebase-nextjs/dist/authpages'); 44 | if (!fs.existsSync(nextFireTarget)) fs.mkdirSync(nextFireTarget); 45 | copyFiles(nextFireSource, nextFireTarget); 46 | console.log("Files copied to app router"); 47 | } 48 | else { 49 | console.error("Pages router not supported yet.") 50 | console.error("See progress here : https://github.com/NirmalScaria/firebase-nextjs/issues/3") 51 | throw new Error("Pages router not supported yet."); 52 | } 53 | 54 | // COPY THE MIDDLEWARE 55 | source = path.join(cwd, 'node_modules/firebase-nextjs/dist/middleware.js'); 56 | if (isUsingSrc) { 57 | target = path.join(cwd, 'src/middleware.js'); 58 | } 59 | else { 60 | target = path.join(cwd, 'middleware.js'); 61 | } 62 | if (fs.existsSync(target)) { 63 | target = path.join(cwd, 'middleware-example.js'); 64 | } 65 | 66 | fs.copyFileSync(source, target); 67 | } -------------------------------------------------------------------------------- /scripts/a_setupGcloud.mjs: -------------------------------------------------------------------------------- 1 | import { google } from 'googleapis'; 2 | import open from 'open'; 3 | import express from 'express'; 4 | import { oauthDetails } from "./oauthDetails.mjs" 5 | 6 | const app = express(); 7 | 8 | export async function setupGcloud() { 9 | const authCreds = await googleAuth(); 10 | const auth = new google.auth.OAuth2(); 11 | auth.setCredentials(authCreds); 12 | return auth; 13 | } 14 | 15 | const encoder = new TextEncoder(); 16 | 17 | function googleAuth() { 18 | var server; 19 | return new Promise(async (resolve, reject) => { 20 | const { authUrl, oauth2Client } = await createAuthClient(); 21 | console.log("🔐 Logging into GCloud. Please authenticate from browser when prompted. 🔐"); 22 | open(authUrl) 23 | app.get('/', async (req, res) => { 24 | const code = req.query.code; 25 | 26 | if (!code) { 27 | return res.status(400).send('Authorization code is missing'); 28 | } 29 | 30 | try { 31 | const { tokens } = await oauth2Client.getToken(code); 32 | oauth2Client.setCredentials(tokens); 33 | const oauth2 = google.oauth2({ 34 | auth: oauth2Client, 35 | version: 'v2' 36 | }); 37 | 38 | var userInfo = await (oauth2.userinfo.get()); 39 | userInfo = encoder.encode(JSON.stringify(userInfo.data)) 40 | userInfo = btoa(String.fromCharCode(...userInfo)) 41 | 42 | const responseContent = `` 43 | res.send(responseContent); 44 | server.close() 45 | resolve(tokens); 46 | } catch (error) { 47 | console.error('Error retrieving access token', error); 48 | res.status(500).json({ success: false, error: error.message }); 49 | } 50 | }); 51 | 52 | server = app.listen(8085); 53 | }) 54 | } 55 | 56 | async function createAuthClient() { 57 | const redirectUri = 'http://localhost:8085/'; 58 | 59 | const oauth2Client = new google.auth.OAuth2(oauthDetails.clientId, oauthDetails.clientSecret, redirectUri); 60 | 61 | const scopes = [ 62 | 'openid', 63 | 'https://www.googleapis.com/auth/cloud-platform', 64 | 'https://www.googleapis.com/auth/userinfo.email', 65 | ]; 66 | 67 | const authUrl = oauth2Client.generateAuthUrl({ 68 | access_type: 'offline', 69 | scope: scopes 70 | }); 71 | 72 | return { authUrl, oauth2Client }; 73 | } -------------------------------------------------------------------------------- /scripts/b_setupProject.mjs: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | import { google } from 'googleapis'; 3 | 4 | export async function setupProject(auth) { 5 | const projects = await getProjects(auth); 6 | const selectedProject = await selectProject(projects); 7 | return selectedProject 8 | } 9 | 10 | async function getProjects(auth) { 11 | const cloudresourcemanager = google.cloudresourcemanager({ 12 | version: 'v1', 13 | auth 14 | }); 15 | 16 | const projects = await cloudresourcemanager.projects.list({ 17 | filter: 'labels.firebase:enabled', 18 | }); 19 | 20 | return projects.data.projects; 21 | } 22 | 23 | async function selectProject(projects) { 24 | const projectChoices = projects.map((project) => { 25 | return { 26 | name: project.name + '(' + project.projectId + ')', 27 | value: project.projectId, 28 | } 29 | }); 30 | 31 | const selectedProject = await inquirer.prompt([ 32 | { 33 | type: 'list', 34 | name: 'project', 35 | message: 'Select a Firebase project', 36 | choices: projectChoices, 37 | } 38 | ]); 39 | 40 | return selectedProject.project; 41 | } -------------------------------------------------------------------------------- /scripts/c_generateServiceAccount.mjs: -------------------------------------------------------------------------------- 1 | // This is to generate a service account credential for firebase 2 | 3 | import inquirer from 'inquirer'; 4 | import path from 'path'; 5 | import { google } from 'googleapis'; 6 | import fs from 'fs'; 7 | 8 | const SERVICE_ACCOUNT_CREDS_LOCATION = path.join(process.cwd(), "firebase-service-account.json"); 9 | 10 | export async function generateServiceAccount(selectedProject, auth) { 11 | const serviceAccounts = await getServiceAccounts(selectedProject, auth); 12 | const selectedServiceAccount = await selectServiceAccount(serviceAccounts); 13 | await storeKey(selectedServiceAccount, selectedProject, auth); 14 | } 15 | 16 | async function getServiceAccounts(selectedProject, auth) { 17 | const iam = google.iam({ 18 | version: 'v1', 19 | auth 20 | }); 21 | 22 | const serviceAccounts = await iam.projects.serviceAccounts.list({ 23 | name: `projects/${selectedProject}`, 24 | }); 25 | 26 | return serviceAccounts.data.accounts; 27 | } 28 | 29 | async function selectServiceAccount(serviceAccounts) { 30 | const serviceAccountChoices = serviceAccounts.map((serviceAccount) => { 31 | return { 32 | name: serviceAccount.displayName + " (" + serviceAccount.email + ")", 33 | value: serviceAccount.email, 34 | } 35 | }); 36 | 37 | const selectedServiceAccount = await inquirer.prompt([ 38 | { 39 | type: 'list', 40 | name: 'serviceAccount', 41 | message: 'Select a service account (firebase-adminsdk is recommended)', 42 | choices: serviceAccountChoices, 43 | } 44 | ]); 45 | 46 | return selectedServiceAccount.serviceAccount; 47 | } 48 | 49 | async function storeKey(serviceAccount, selectedProject, auth) { 50 | const iam = google.iam({ 51 | version: 'v1', 52 | auth 53 | }); 54 | const resp = await iam.projects.serviceAccounts.keys.create({ 55 | name: `projects/${selectedProject}/serviceAccounts/${serviceAccount}`, 56 | }); 57 | const key = resp.data.privateKeyData; 58 | const keyStr = Buffer.from(key, 'base64').toString('utf-8'); 59 | fs.writeFileSync(SERVICE_ACCOUNT_CREDS_LOCATION, keyStr); 60 | // wait a second so that service account is ready and file is written 61 | await new Promise((resolve) => setTimeout(resolve, 1000)); 62 | return; 63 | } -------------------------------------------------------------------------------- /scripts/cliUtils.mjs: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import readline from "readline"; 3 | 4 | const steps = [ 5 | "Install authentication components", 6 | "Setup GCloud", 7 | "Setup Firebase Project", 8 | "Generate Service Account", 9 | "Set Web App", 10 | "Enable Auth" 11 | ] 12 | 13 | const clearLines = (lines) => { 14 | readline.moveCursor(process.stdout, 0, -lines); 15 | readline.clearScreenDown(process.stdout); 16 | }; 17 | 18 | export function showStepsStatus(currentStep) { 19 | clearLines(steps.length + 100) 20 | console.log("🤞🏻 FirebaseNextJS Setup Steps. 🤞🏻") 21 | steps.forEach((step, index) => { 22 | if (index < currentStep) { 23 | console.log(chalk.green("✓ " + step)); 24 | } else if (index === currentStep) { 25 | console.log(chalk.blueBright("⠋ "), step); 26 | } else { 27 | console.log(chalk.gray(`• ${step}`)); 28 | } 29 | }); 30 | }; -------------------------------------------------------------------------------- /scripts/d_setWebApp.mjs: -------------------------------------------------------------------------------- 1 | // This will set up a web app for the firebase project 2 | 3 | import { google } from "googleapis"; 4 | const firebase = google.firebase('v1beta1'); 5 | import inquirer from "inquirer"; 6 | import path from "path"; 7 | import fs from "fs"; 8 | 9 | const WEB_APP_CREDS_LOCATION = path.join(process.cwd(), "firebase-app-config.js"); 10 | 11 | export async function setWebApp(selectedProject) { 12 | await setupGoogleAuth(google) 13 | var apps = (await firebase.projects.webApps.list({ 14 | parent: `projects/${selectedProject}` 15 | })).data.apps; 16 | const selectedApp = await selectApp(apps, selectedProject); 17 | await saveCreds(selectedProject, selectedApp); 18 | } 19 | 20 | export async function setupGoogleAuth(googleObject) { 21 | const auth = new googleObject.auth.GoogleAuth({ 22 | scopes: [ 23 | 'https://www.googleapis.com/auth/cloud-platform', 24 | 'https://www.googleapis.com/auth/firebase', 25 | ], 26 | keyFilename: 'firebase-service-account.json' 27 | }) 28 | const authClient = await auth.getClient(); 29 | googleObject.options({ auth: authClient }); 30 | } 31 | 32 | async function selectApp(apps, selectedProject) { 33 | var appChoices = []; 34 | if (apps == undefined || apps.length === 0) { 35 | console.log("No web apps found in the project! Please create an app."); 36 | } 37 | else { 38 | appChoices = apps.map((app) => { 39 | return { 40 | name: app.displayName + '(' + app.appId + ')', 41 | value: app.appId, 42 | } 43 | }); 44 | } 45 | appChoices.push({ name: "Create a New Web App", value: "createnewappnow" }); 46 | 47 | const selectedApp = await inquirer.prompt([ 48 | { 49 | type: 'list', 50 | name: 'selectedApp', 51 | message: 'Select an app', 52 | choices: appChoices 53 | } 54 | ]); 55 | 56 | if (selectedApp.selectedApp === "createnewappnow") { 57 | if (await createWebApp(apps, selectedProject) == "created") { 58 | apps = (await firebase.projects.webApps.list({ 59 | parent: `projects/${selectedProject}` 60 | })).data.apps; 61 | return selectApp(apps, selectedProject); 62 | } 63 | } 64 | return selectedApp.selectedApp; 65 | } 66 | 67 | // NOTE: Create function call will not return the appId. It instead returns a workflow. 68 | // Instead of checking the workflow, the idea is to keep checking for apps list until the new app is created. 69 | async function createWebApp(apps, selectedProject) { 70 | const oldAppCount = apps ? apps.length : 0; 71 | const newApp = await inquirer.prompt([ 72 | { 73 | type: 'input', 74 | name: 'displayName', 75 | message: 'Enter the display name for the new app : ', 76 | default: "My FirebaseNextJS App" 77 | }, 78 | ]) 79 | const resp = await firebase.projects.webApps.create({ 80 | parent: `projects/${selectedProject}`, 81 | requestBody: { 82 | displayName: newApp.displayName, 83 | appUrls: [], 84 | } 85 | }) 86 | console.log("🧑‍🍳 Creating app 🧑‍🍳") 87 | for (let i = 0; i < 15; i++) { 88 | const newApps = (await firebase.projects.webApps.list({ 89 | parent: `projects/${selectedProject}` 90 | })).data.apps; 91 | if (newApps != undefined && (newApps.length > oldAppCount)) { 92 | console.log("👶🏻 App created 👶🏻") 93 | return "created" 94 | } 95 | await new Promise((resolve) => setTimeout(resolve, 1500)); 96 | } 97 | throw Error("App creation failed. Please try again.") 98 | } 99 | 100 | async function saveCreds(selectedProject, selectedApp) { 101 | var config = (await firebase.projects.webApps.getConfig({ 102 | name: `projects/${selectedProject}/webApps/${selectedApp}/config` 103 | })).data 104 | config = "export const firebaseConfig = " + JSON.stringify(config, null, 2) 105 | config = `import { initializeApp } from "firebase/app";\n\n` + config 106 | config = config + `\n\nexport const firebaseApp = initializeApp(firebaseConfig);` 107 | fs.writeFileSync(WEB_APP_CREDS_LOCATION, config); 108 | } -------------------------------------------------------------------------------- /scripts/e_enableAuth.mjs: -------------------------------------------------------------------------------- 1 | // This enable the authentication providers (email - password and google) for the firebase project 2 | 3 | import { google } from "googleapis"; 4 | import { setupGoogleAuth } from "./d_setWebApp.mjs"; 5 | import readline from "readline"; 6 | const identitytoolkit = google.identitytoolkit('v2'); 7 | import open from "open"; 8 | import chalk from "chalk"; 9 | 10 | export async function enableAuth(selectedProject) { 11 | await setupGoogleAuth(google) 12 | await enableEmailPassword(selectedProject); 13 | } 14 | 15 | async function enableEmailPassword(selectedProject) { 16 | try { 17 | const resp = await identitytoolkit.projects.updateConfig({ 18 | name: `projects/${selectedProject}/config`, 19 | updateMask: "signIn.email.enabled,signIn.email.passwordRequired", 20 | requestBody: { 21 | signIn: { 22 | email: { 23 | enabled: true, 24 | passwordRequired: true 25 | } 26 | } 27 | } 28 | }) 29 | } 30 | catch (error) { 31 | console.log("Please enable Authentication (Click 'Get Started') from Firebase Console. (You don't have to do any setup there)") 32 | console.log(chalk.green("https://console.firebase.google.com/project/" + selectedProject + "/authentication")) 33 | open("https://console.firebase.google.com/project/" + selectedProject + "/authentication") 34 | const rl = readline.createInterface({ 35 | input: process.stdin, 36 | output: process.stdout 37 | }); 38 | await new Promise(resolve => { 39 | rl.question("Press enter to continue ", resolve) 40 | }) 41 | // Finally I get to use recursion for something useful. 42 | await enableEmailPassword(selectedProject) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /scripts/index.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { setup } from "./setup.mjs"; 4 | import { getenv } from "./utils/getEnv.mjs"; 5 | 6 | import { Command } from "commander" 7 | 8 | async function main() { 9 | console.log("🔥🔥🔥 Welcome to FirebaseNextJS 🔥🔥🔥") 10 | const program = new Command() 11 | .name("FirebaseNextJS") 12 | .description("FirebaseNextJS is a CLI tool to help you set up Firebase with Next.js") 13 | 14 | program.addCommand(setup).addCommand(getenv) 15 | 16 | program.parse() 17 | } 18 | 19 | main() -------------------------------------------------------------------------------- /scripts/setup.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { copyComponents } from "./a_copyComponents.mjs"; 4 | import { setupGcloud } from "./a_setupGcloud.mjs"; 5 | import { setupProject } from "./b_setupProject.mjs"; 6 | import { generateServiceAccount } from "./c_generateServiceAccount.mjs"; 7 | import { setWebApp } from "./d_setWebApp.mjs"; 8 | import { enableAuth } from "./e_enableAuth.mjs"; 9 | import { showStepsStatus } from "./cliUtils.mjs"; 10 | import { Command } from "commander"; 11 | 12 | export const setup = new Command("setup") 13 | .description("Setup FirebaseNextJS") 14 | .action(setupAction); 15 | 16 | async function setupAction() { 17 | console.log("🤞🏻 Starting FirebaseNextJS Setup. 🤞🏻") 18 | 19 | // Step 0 : Copy the files 20 | showStepsStatus(0) 21 | console.log("📦 Installing authentication components 📦") 22 | await copyComponents(); 23 | console.log("👏 Components installed. 👏") 24 | 25 | // Step 1 : Install gcloud and login to it. 26 | showStepsStatus(1) 27 | console.log("🤖 Setting up GCloud 🤖") 28 | const auth = await setupGcloud(); 29 | console.log("👏 GCloud setup complete. 👏") 30 | 31 | // Step 2 : Set firebase project 32 | showStepsStatus(2) 33 | console.log("👀 Checking available firebase projects 👀") 34 | const selectedProject = await setupProject(auth); 35 | console.log(`🫰🏼 Project Setup Complete: ${selectedProject} 🫰🏼`) 36 | 37 | // Step 3 : Generate service account 38 | showStepsStatus(3) 39 | console.log("🤖 Setting up service account 🤖") 40 | await generateServiceAccount(selectedProject, auth); 41 | console.log("👏 Service Account setup complete. 👏") 42 | 43 | // Step 4 : Set up firebase app 44 | showStepsStatus(4) 45 | console.log("🤓 Checking registered apps in the firebase project 🤓") 46 | await setWebApp(selectedProject); 47 | console.log("👏 Firebase App setup complete. 👏") 48 | 49 | console.log("🫃🏻 Almost there... Enabling authentication providers...") 50 | showStepsStatus(5) 51 | await enableAuth(selectedProject) 52 | showStepsStatus(6) 53 | console.log("Email-password authentication enabled. If you want to enable Google Authentication, please visit and click add provider:") 54 | console.log("https://console.firebase.google.com/u/0/project/" + selectedProject + "/authentication/providers") 55 | console.log("🎉🎉🎉 Setup Complete 🎉🎉🎉") 56 | } 57 | 58 | // setupAction(); -------------------------------------------------------------------------------- /scripts/utils/getEnv.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | import { Command } from "commander"; 6 | 7 | export const getenv = new Command("getenv") 8 | .description("Get the environment variables for the service account credentials") 9 | .action(getenvAction); 10 | 11 | async function getenvAction() { 12 | const serviceAccountCreds = await getServiceAccountCreds(); 13 | const credString = serviceAccountCreds; 14 | console.log("Here are the environment variables. Please copy these and paste them in your environment.") 15 | console.log("Make sure to either delete or gitignore the firebase-service-account.json file before commiting to version control.\n\n") 16 | console.log(credString) 17 | console.log("\n\n") 18 | } 19 | 20 | async function getServiceAccountCreds() { 21 | const SERVICE_ACCOUNT_CREDS_LOCATION = path.join(process.cwd(), "firebase-service-account.json"); 22 | const key = JSON.stringify(JSON.parse(fs.readFileSync(SERVICE_ACCOUNT_CREDS_LOCATION, 'utf8'))); 23 | return `FIREBASENEXTJS_SERVICE_ACCOUNT_CREDENTIALS='${key}'` 24 | } -------------------------------------------------------------------------------- /server/auth.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { cookies } from 'next/headers' 3 | import * as admin from 'firebase-admin'; 4 | import { DecodedIdToken, getAuth } from "firebase-admin/auth"; 5 | import fs from 'fs'; 6 | 7 | async function getServiceAccountCreds() { 8 | if (process.env.FIREBASENEXTJS_SERVICE_ACCOUNT_CREDENTIALS) { 9 | return JSON.parse(process.env.FIREBASENEXTJS_SERVICE_ACCOUNT_CREDENTIALS) 10 | } 11 | else { 12 | try { 13 | const serverConfigFile = fs.readFileSync('firebase-service-account.json', 'utf8') 14 | return JSON.parse(serverConfigFile) 15 | } 16 | catch (error) { 17 | console.error("Error while reading service account creds", error) 18 | return null 19 | } 20 | } 21 | } 22 | 23 | export async function getAppSS() { 24 | var app; 25 | if (admin.apps.length === 0) { 26 | app = admin.initializeApp({ 27 | credential: admin.credential.cert(await getServiceAccountCreds()) 28 | }); 29 | } else { 30 | app = admin.app() 31 | } 32 | return app 33 | } 34 | 35 | export async function getUserSS() { 36 | var app; 37 | if (admin.apps.length === 0) { 38 | app = admin.initializeApp({ 39 | credential: admin.credential.cert(await getServiceAccountCreds()) 40 | }); 41 | } else { 42 | app = admin.app() 43 | } 44 | const cookieStore = cookies() 45 | const token = cookieStore.get('firebase_nextjs_token') 46 | if (token === undefined) { 47 | return null 48 | } 49 | try { 50 | const user: DecodedIdToken = await getAuth(app) 51 | .verifySessionCookie(token.value, true) 52 | return user; 53 | } catch (error) { 54 | console.error('Error while verifying token', error); 55 | return null 56 | } 57 | } -------------------------------------------------------------------------------- /server/getToken.d.ts: -------------------------------------------------------------------------------- 1 | export declare const getToken: ({ idToken }: { idToken: string }) => Promise; -------------------------------------------------------------------------------- /server/getToken.js: -------------------------------------------------------------------------------- 1 | "use server" 2 | import { getAuth } from "firebase-admin/auth"; 3 | import { getAppSS } from "./auth" 4 | 5 | /** 6 | * Takes a temporary token from firebase getIdToken and returns 7 | * a token that can be used to authenticate with the server. 8 | */ 9 | export async function getToken({ idToken }) { 10 | const app = await getAppSS(); 11 | // Token expires in 14 days 12 | const expiresIn = 60 * 60 * 24 * 14 * 1000; 13 | const sessionCookie = getAuth().createSessionCookie(idToken, { expiresIn }); 14 | return sessionCookie; 15 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "jsx": "react", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "outDir": ".", 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true 13 | }, 14 | "include": ["./**/*.*", "./**/*.*"], 15 | "exclude": ["node_modules", "build"] 16 | } -------------------------------------------------------------------------------- /utilities/createSymlink.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // This will create a symlink from build/node_modules to node_modules 4 | 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | 8 | fs.symlinkSync(path.join(process.cwd(), 'node_modules'), path.join(process.cwd(), 'build', 'node_modules'), 'dir'); -------------------------------------------------------------------------------- /utilities/deleteSymlink.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // This will delete the symlink from build/node_modules to node_modules 4 | 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | 8 | fs.unlinkSync(path.join(process.cwd(), 'build', 'node_modules')); 9 | 10 | console.log("Symlink deleted") -------------------------------------------------------------------------------- /utilities/preparePackage.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // This will take the package.json file from build/package.json, and edit 4 | // the file to remove the "private" field 5 | // This is necessary for publishing the package to npm. 6 | 7 | import fs from 'fs'; 8 | import path from 'path'; 9 | 10 | const packagePath = path.resolve('build/package.json'); 11 | const packageInfo = JSON.parse(fs.readFileSync(packagePath, 'utf8')); 12 | 13 | delete packageInfo.private; 14 | 15 | fs.writeFileSync(packagePath, JSON.stringify(packageInfo, null, 2)); 16 | --------------------------------------------------------------------------------