├── .nvmrc ├── src ├── css │ └── global.css ├── components │ ├── base │ │ ├── button │ │ │ ├── index.ts │ │ │ └── Button.svelte │ │ ├── input │ │ │ ├── index.ts │ │ │ └── Input.svelte │ │ └── popover │ │ │ ├── index.ts │ │ │ └── Popover.svelte │ ├── LoadingButton.svelte │ ├── icons │ │ ├── Mail.svelte │ │ ├── Spinner.svelte │ │ ├── LogoutIcon.svelte │ │ ├── DashboardIcon.svelte │ │ └── GoogleIcon.svelte │ ├── social │ │ └── GoogleSignInButton.svelte │ ├── Dashboard.svelte │ ├── Navbar.svelte │ ├── UserDropdown.svelte │ ├── Home.svelte │ ├── ForgetPasswordForm.svelte │ ├── ConfirmPasswordForm.svelte │ ├── SignupForm.svelte │ ├── EditAccountForm.svelte │ └── SigninForm.svelte ├── pages │ ├── api │ │ ├── auth │ │ │ ├── signout.ts │ │ │ └── signin.ts │ │ └── user │ │ │ └── update.ts │ ├── index.astro │ ├── signup │ │ └── index.astro │ ├── signin │ │ └── index.astro │ ├── account │ │ └── index.astro │ ├── dashboard │ │ └── index.astro │ └── forgot-password │ │ ├── index.astro │ │ └── confirm │ │ └── index.astro ├── firebase │ ├── client.ts │ ├── utils │ │ └── auth │ │ │ ├── isLoggedIn.ts │ │ │ ├── updateUserData.ts │ │ │ ├── getUserData.ts │ │ │ ├── handleGoogleSignIn.ts │ │ │ ├── getFriendlyErrorMessage.ts │ │ │ └── handleSignup.ts │ └── server.ts ├── env.d.ts ├── services │ └── api │ │ ├── fetchSignIn.ts │ │ └── fetchUpdateUser.ts ├── layouts │ └── Layout.astro └── lib │ └── actions.ts ├── .vscode ├── extensions.json └── launch.json ├── svelte.config.js ├── .github └── assets │ └── firebase-starter-banner.png ├── .env.example ├── tsconfig.json ├── .gitignore ├── astro.config.mjs ├── public ├── favicon.svg └── astro.svg ├── package.json ├── tailwind.config.js ├── LICENSE.txt ├── scripts ├── README.md └── createEnvFromServiceAccount.js ├── README.md └── docs └── FIREBASE_SETUP.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 -------------------------------------------------------------------------------- /src/css/global.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #fbfcff; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/base/button/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Button } from "./Button.svelte"; 2 | -------------------------------------------------------------------------------- /src/components/base/input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Input } from "./Input.svelte"; 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@astrojs/svelte'; 2 | 3 | export default { 4 | preprocess: vitePreprocess(), 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/base/popover/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Popover } from "./Popover.svelte"; 2 | // Re-export the named export 'bindTrigger' 3 | -------------------------------------------------------------------------------- /.github/assets/firebase-starter-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Porter-smith/astro-firebase-svelte-tailwind-starter/HEAD/.github/assets/firebase-starter-banner.png -------------------------------------------------------------------------------- /src/pages/api/auth/signout.ts: -------------------------------------------------------------------------------- 1 | import type { APIRoute } from "astro"; 2 | 3 | export const GET: APIRoute = ({ redirect, cookies }) => { 4 | cookies.delete("session", { 5 | path: "/", 6 | }); 7 | return redirect("/"); 8 | }; 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | 2 | # Firebase 3 | FIREBASE_PRIVATE_KEY_ID= 4 | FIREBASE_PRIVATE_KEY= 5 | FIREBASE_PROJECT_ID= 6 | FIREBASE_CLIENT_EMAIL= 7 | FIREBASE_CLIENT_ID= 8 | FIREBASE_AUTH_URI= 9 | FIREBASE_TOKEN_URI= 10 | FIREBASE_AUTH_CERT_URL= 11 | FIREBASE_CLIENT_CERT_URL= -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "target": "ES2020", 5 | "module": "ES2020", 6 | "jsx": "react-jsx", 7 | "jsxImportSource": "react", 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["src/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/LoadingButton.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 13 | -------------------------------------------------------------------------------- /src/components/icons/Mail.svelte: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | # Firebase service account to prevent accidentally commiting it 24 | service-account.json 25 | .vercel -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import tailwind from "@astrojs/tailwind"; 3 | import svelte from "@astrojs/svelte"; 4 | 5 | import vercel from "@astrojs/vercel/serverless"; 6 | 7 | // https://astro.build/config 8 | export default defineConfig({ 9 | integrations: [tailwind(), svelte()], 10 | output: "server", 11 | server: { 12 | port: 3000 13 | }, 14 | preview: { 15 | port: 3000 16 | }, 17 | adapter: vercel() 18 | }); -------------------------------------------------------------------------------- /src/firebase/client.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | 3 | const firebaseConfig = { 4 | apiKey: "AIzaSyBu6KkVzDWVgYPf4UPM08rFowqy5ayNMTc", 5 | authDomain: "fir-astro-starter.firebaseapp.com", 6 | projectId: "fir-astro-starter", 7 | storageBucket: "fir-astro-starter.appspot.com", 8 | messagingSenderId: "996337362101", 9 | appId: "1:996337362101:web:47d0d82c326e84fe29cafc", 10 | measurementId: "G-ZLM4QPD3KQ", 11 | }; 12 | 13 | export const app = initializeApp(firebaseConfig); 14 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | interface ImportMetaEnv { 2 | readonly FIREBASE_PRIVATE_KEY_ID: string; 3 | readonly FIREBASE_PRIVATE_KEY: string; 4 | readonly FIREBASE_PROJECT_ID: string; 5 | readonly FIREBASE_CLIENT_EMAIL: string; 6 | readonly FIREBASE_CLIENT_ID: string; 7 | readonly FIREBASE_AUTH_URI: string; 8 | readonly FIREBASE_TOKEN_URI: string; 9 | readonly FIREBASE_AUTH_CERT_URL: string; 10 | readonly FIREBASE_CLIENT_CERT_URL: string; 11 | } 12 | 13 | interface ImportMeta { 14 | readonly env: ImportMetaEnv; 15 | } 16 | -------------------------------------------------------------------------------- /src/services/api/fetchSignIn.ts: -------------------------------------------------------------------------------- 1 | async function fetchSignIn(idToken: string) { 2 | const response = await fetch("/api/auth/signin", { 3 | method: "GET", 4 | headers: { 5 | Authorization: `Bearer ${idToken}`, 6 | }, 7 | }); 8 | 9 | if (!response.ok) { 10 | throw new Error( 11 | `Failed to sign in. Server responded with status: ${response.status}` 12 | ); 13 | } 14 | 15 | return response.redirected ? response.url : null; // Include URL if redirected 16 | } 17 | 18 | export default fetchSignIn; 19 | -------------------------------------------------------------------------------- /src/components/icons/Spinner.svelte: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/components/base/input/Input.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "@/layouts/Layout.astro"; 3 | import Home from "../components/Home.svelte"; 4 | import Navbar from "../components/Navbar.svelte"; 5 | import getUserData from "@/firebase/utils/auth/getUserData"; 6 | 7 | const title = "Home - Astro Starter Pack"; 8 | const description = 9 | "Seamlessly integrate Firebase Auth with TailwindCSS and Svelte - start building secure and stylish applications with ease."; 10 | const sessionCookie = Astro.cookies.get("session")?.value ?? null; 11 | const user = await getUserData(sessionCookie); 12 | --- 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/pages/signup/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import SignupForm from "@/components/SignupForm.svelte"; 3 | import Layout from "@/layouts/Layout.astro"; 4 | import Navbar from "../../components/Navbar.svelte"; 5 | import getUserData from "../../firebase/utils/auth/getUserData"; 6 | const title = "Sign Up - Astro Starter Pack"; 7 | const description = 8 | "Join Astro Starter Pack today and start enjoying all the benefits of our member-exclusive features and content."; 9 | const sessionCookie = Astro.cookies.get("session")?.value ?? null; 10 | const user = await getUserData(sessionCookie); 11 | --- 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/services/api/fetchUpdateUser.ts: -------------------------------------------------------------------------------- 1 | async function fetchUpdateUser(userData: object) { 2 | const response = await fetch("/api/user/update", { 3 | // Assuming this is your endpoint for updating user data 4 | method: "POST", 5 | headers: { 6 | "Content-Type": "application/json", 7 | }, 8 | body: JSON.stringify(userData), // Make sure to send the user data as JSON 9 | }); 10 | const data = await response.json(); 11 | 12 | if (!response.ok) { 13 | throw new Error( 14 | data.error || 15 | "Failed to update user. Server responded with status: ${response.status}" 16 | ); 17 | } 18 | 19 | return data.data; // Return the JSON data 20 | } 21 | 22 | export default fetchUpdateUser; 23 | -------------------------------------------------------------------------------- /src/components/icons/LogoutIcon.svelte: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /src/components/social/GoogleSignInButton.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | -------------------------------------------------------------------------------- /src/pages/signin/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import SigninForm from "@/components/SigninForm.svelte"; 3 | import Layout from "@/layouts/Layout.astro"; 4 | import getUserData from "../../firebase/utils/auth/getUserData"; 5 | import Navbar from "../../components/Navbar.svelte"; 6 | const title = "Sign In - Astro Starter Pack"; 7 | const description = 8 | "Sign in to your account to access personalized settings and features."; 9 | 10 | const sessionCookie = Astro.cookies.get("session")?.value ?? null; 11 | const user = await getUserData(sessionCookie); 12 | if (user) { 13 | return Astro.redirect("/dashboard"); 14 | } 15 | --- 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/pages/account/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "@/layouts/Layout.astro"; 3 | import Navbar from "../../components/Navbar.svelte"; 4 | import getUserData from "../../firebase/utils/auth/getUserData"; 5 | import EditAccountForm from "../../components/EditAccountForm.svelte"; 6 | 7 | /* Check current session */ 8 | const sessionCookie = Astro.cookies.get("session")?.value ?? null; 9 | 10 | const user = await getUserData(sessionCookie); 11 | 12 | if (!user) { 13 | return Astro.redirect("/signin"); 14 | } 15 | const title = "Account - Astro Starter Pack"; 16 | const description = "Account page to manage account settings"; 17 | --- 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/pages/dashboard/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "@/layouts/Layout.astro"; 3 | import Dashboard from "../../components/Dashboard.svelte"; 4 | import Navbar from "../../components/Navbar.svelte"; 5 | import getUserData from "../../firebase/utils/auth/getUserData"; 6 | 7 | /* Check current session */ 8 | const sessionCookie = Astro.cookies.get("session")?.value ?? null; 9 | 10 | const user = await getUserData(sessionCookie); 11 | 12 | if (!user) { 13 | return Astro.redirect("/signin"); 14 | } 15 | 16 | const title = "Dashboard - Astro Starter Pack"; 17 | const description = "Dashboard page to view and manage firebase data"; 18 | --- 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/pages/forgot-password/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import ForgetPasswordForm from "@/components/ForgetPasswordForm.svelte"; 3 | import Layout from "@/layouts/Layout.astro"; 4 | import getUserData from "../../firebase/utils/auth/getUserData"; 5 | import Navbar from "../../components/Navbar.svelte"; 6 | const title = "Forgot Password - Astro Starter Pack"; 7 | const description = "Reset your password to regain access to your account."; 8 | 9 | const sessionCookie = Astro.cookies.get("session")?.value ?? null; 10 | const user = await getUserData(sessionCookie); 11 | if (user) { 12 | return Astro.redirect("/dashboard"); 13 | } 14 | --- 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-firebase-svelte-tailwind-starter", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro check && astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/check": "^0.3.1", 14 | "@astrojs/svelte": "^4.0.3", 15 | "@astrojs/tailwind": "^5.0.2", 16 | "@astrojs/vercel": "^5.1.0", 17 | "astro": "^3.4.3", 18 | "encoding": "^0.1.13", 19 | "firebase": "^9.22.2", 20 | "firebase-admin": "^11.9.0", 21 | "request": "^2.88.2", 22 | "svelte": "^4.0.0", 23 | "tailwindcss": "^3.0.24", 24 | "tailwindcss-animate": "^1.0.7", 25 | "typescript": "^5.2.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], 4 | theme: { 5 | extend: { 6 | animation: { 7 | // Tooltip 8 | "slide-up-fade": "slide-up-fade 0.3s cubic-bezier(0.16, 1, 0.3, 1)", 9 | "slide-down-fade": "slide-down-fade 0.3s cubic-bezier(0.16, 1, 0.3, 1)", 10 | }, 11 | keyframes: { 12 | // Tooltip 13 | "slide-up-fade": { 14 | "0%": { opacity: 0, transform: "translateY(6px)" }, 15 | "100%": { opacity: 1, transform: "translateY(0)" }, 16 | }, 17 | "slide-down-fade": { 18 | "0%": { opacity: 0, transform: "translateY(-6px)" }, 19 | "100%": { opacity: 1, transform: "translateY(0)" }, 20 | }, 21 | }, 22 | }, 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/forgot-password/confirm/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import ConfirmPasswordForm from "@/components/ConfirmPasswordForm.svelte"; 3 | import Layout from "@/layouts/Layout.astro"; 4 | import getUserData from "@/firebase/utils/auth/getUserData"; 5 | import Navbar from "@/components/Navbar.svelte"; 6 | const title = "Confirm Password - Astro Starter Pack"; 7 | const description = "Reset your password to regain access to your account."; 8 | 9 | const sessionCookie = Astro.cookies.get("session")?.value ?? null; 10 | const user = await getUserData(sessionCookie); 11 | if (user) { 12 | return Astro.redirect("/dashboard"); 13 | } 14 | const oobCode = Astro.url.searchParams.get("oobCode")! || ""; 15 | --- 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/Dashboard.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
11 |
12 |
13 |

Dashboard

14 |

Welcome, {user.displayName}!

15 |

We are happy to see you here. Ready to dive in?

16 |
17 | 18 |
19 |
20 |
21 |
22 | 23 | 26 | -------------------------------------------------------------------------------- /src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { ViewTransitions } from "astro:transitions"; 3 | import "@/css/global.css"; 4 | // Define your Props interface with both `title` and `description` 5 | interface Props { 6 | title: string; 7 | description: string; 8 | } 9 | 10 | // Extract props from Astro.props 11 | const { title, description } = Astro.props as Props; 12 | --- 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {title} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/components/icons/DashboardIcon.svelte: -------------------------------------------------------------------------------- 1 | 45 | -------------------------------------------------------------------------------- /src/firebase/utils/auth/isLoggedIn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if a user is logged in by verifying the session cookie. 3 | * 4 | * @param {string} cookie - The session cookie to verify. 5 | * @returns {Promise} - A promise that resolves to true if the user is logged in, otherwise false. 6 | * @throws {Error} - If an error occurs during the verification of the session cookie. 7 | */ 8 | import { getAuth } from "firebase-admin/auth"; 9 | import { app } from "@/firebase/server"; 10 | 11 | async function isLoggedIn(cookie: string): Promise { 12 | const auth = getAuth(app); 13 | 14 | if (cookie) { 15 | try { 16 | const decodedCookie = await auth.verifySessionCookie(cookie); 17 | if (decodedCookie) { 18 | return true; // if session cookie is valid, return true 19 | } 20 | } catch (error) { 21 | console.error("Error verifying session cookie:", error); 22 | // You can handle/log the error as required. 23 | } 24 | } 25 | 26 | return false; 27 | } 28 | export default isLoggedIn; 29 | -------------------------------------------------------------------------------- /src/firebase/server.ts: -------------------------------------------------------------------------------- 1 | import admin from "firebase-admin"; 2 | import type { ServiceAccount } from "firebase-admin"; 3 | // Your service account details 4 | const serviceAccount = { 5 | type: "service_account", 6 | project_id: import.meta.env.FIREBASE_PROJECT_ID, 7 | private_key_id: import.meta.env.FIREBASE_PRIVATE_KEY_ID, 8 | private_key: import.meta.env.FIREBASE_PRIVATE_KEY, 9 | client_email: import.meta.env.FIREBASE_CLIENT_EMAIL, 10 | client_id: import.meta.env.FIREBASE_CLIENT_ID, 11 | auth_uri: import.meta.env.FIREBASE_AUTH_URI, 12 | token_uri: import.meta.env.FIREBASE_TOKEN_URI, 13 | auth_provider_x509_cert_url: import.meta.env.FIREBASE_AUTH_CERT_URL, 14 | client_x509_cert_url: import.meta.env.FIREBASE_CLIENT_CERT_URL, 15 | }; 16 | 17 | // Check if any apps have been initialized. If not, initialize a new app 18 | export const app = admin.apps.length 19 | ? admin.app() // Use the existing app instance 20 | : admin.initializeApp({ 21 | credential: admin.credential.cert(serviceAccount as ServiceAccount), 22 | }); 23 | -------------------------------------------------------------------------------- /src/components/icons/GoogleIcon.svelte: -------------------------------------------------------------------------------- 1 | 9 | 13 | 17 | 21 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/firebase/utils/auth/updateUserData.ts: -------------------------------------------------------------------------------- 1 | import { getAuth } from "firebase-admin/auth"; 2 | import type { UserRecord } from "firebase-admin/auth"; 3 | 4 | /** 5 | * Updates the data for a specified user. 6 | * 7 | * @param {string} uid - The user ID of the Firebase user. 8 | * @param {UserUpdates} updates - An object containing the data to update. 9 | * @returns {Promise} - A promise that resolves with the updated user record. 10 | */ 11 | 12 | type UserUpdates = { 13 | displayName?: string; 14 | email?: string; 15 | emailVerified?: boolean; 16 | // If you want to be able to update more stuff you can add type for that here 17 | }; 18 | 19 | async function updateUserData( 20 | uid: string, 21 | updates: UserUpdates 22 | ): Promise { 23 | const auth = getAuth(); 24 | 25 | if (updates?.email) { 26 | // * When Admin firebase SDK when you change email you need to set emailVerified to true 27 | updates.emailVerified = false; 28 | } 29 | 30 | // Perform the update with the modified updates object. 31 | return await auth.updateUser(uid, updates); 32 | } 33 | 34 | export default updateUserData; 35 | -------------------------------------------------------------------------------- /src/firebase/utils/auth/getUserData.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves user data based on the provided session cookie. 3 | * @param {string} cookie - The session cookie string to verify and extract user data from. 4 | * @returns {Promise} - A promise that resolves with the user data if authenticated, otherwise null. 5 | */ 6 | 7 | import { getAuth } from "firebase-admin/auth"; 8 | import { app } from "@/firebase/server"; 9 | 10 | async function getUserData(cookie: string | null) { 11 | // If there's no cookie, return null immediately. 12 | if (cookie === null) { 13 | return null; 14 | } 15 | 16 | const auth = getAuth(app); 17 | 18 | try { 19 | const decodedCookie = await auth.verifySessionCookie(cookie); 20 | const userRecord = await auth.getUser(decodedCookie.uid); // get user data 21 | 22 | // if user exists and is authenticated, return user data 23 | return userRecord; 24 | } catch (error) { 25 | console.error("Error verifying session cookie:", error); 26 | // You can handle/log the error as required. 27 | return null; // Return null in case of any error. 28 | } 29 | } 30 | export default getUserData; 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Porter Smith 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/pages/api/auth/signin.ts: -------------------------------------------------------------------------------- 1 | import type { APIRoute } from "astro"; 2 | import { app } from "@/firebase/server"; 3 | import { getAuth } from "firebase-admin/auth"; 4 | 5 | export const GET: APIRoute = async ({ request, cookies, redirect }) => { 6 | const auth = getAuth(app); 7 | 8 | /* Get token from request headers */ 9 | const idToken = request.headers.get("Authorization")?.split("Bearer ")[1]; 10 | if (!idToken) { 11 | return new Response( 12 | JSON.stringify({ 13 | error: "No token found", 14 | }), 15 | { status: 401 } 16 | ); 17 | } 18 | 19 | /* Verify the token */ 20 | const decodedToken = await auth.verifyIdToken(idToken).catch(() => null); 21 | 22 | if (!decodedToken) { 23 | return new Response( 24 | JSON.stringify({ 25 | error: "Invalid token", 26 | }), 27 | { status: 401 } 28 | ); 29 | } 30 | 31 | const fiveDays = 60 * 60 * 24 * 5 * 1000; 32 | 33 | /* Create and set session cookie */ 34 | const sessionCookie = await auth.createSessionCookie(idToken, { 35 | expiresIn: fiveDays, 36 | }); 37 | 38 | cookies.set("session", sessionCookie, { 39 | path: "/", 40 | }); 41 | 42 | return redirect("/dashboard"); 43 | }; 44 | -------------------------------------------------------------------------------- /src/firebase/utils/auth/handleGoogleSignIn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Initiates a Google sign-in process using Firebase Authentication. 3 | * It authenticates the user, retrieves an ID token, and then attempts to 4 | * establish a session with the server using the ID token. If the server responds 5 | * with a redirection, the browser will navigate to the provided URL. 6 | * 7 | * @throws Will throw an error if the Google sign-in process or the server session setup fails. 8 | */ 9 | import { getAuth, GoogleAuthProvider, signInWithPopup } from "firebase/auth"; 10 | import { app } from "@/firebase/client"; 11 | 12 | async function handleGoogleSignIn() { 13 | const auth = getAuth(app); 14 | try { 15 | const provider = new GoogleAuthProvider(); 16 | const userCredential = await signInWithPopup(auth, provider); 17 | 18 | const idToken = await userCredential.user.getIdToken(); 19 | const res = await fetch("/api/auth/signin", { 20 | method: "GET", 21 | headers: { 22 | Authorization: `Bearer ${idToken}`, 23 | }, 24 | }); 25 | 26 | if (res.redirected) { 27 | window.location.assign(res.url); 28 | } 29 | } catch (error) { 30 | console.error("Error during Google Sign In or setting user role:", error); 31 | } 32 | } 33 | export default handleGoogleSignIn; 34 | -------------------------------------------------------------------------------- /src/firebase/utils/auth/getFriendlyErrorMessage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Translates Firebase error codes into user-friendly messages. 3 | * @param {FirebaseError} error - The error object from Firebase with `code` and `message` properties. 4 | * @returns {string} - A user-friendly error message. 5 | */ 6 | 7 | import type { FirebaseError } from "firebase/app"; 8 | 9 | type FirebaseErrorMapping = { 10 | [key: string]: string; 11 | }; 12 | 13 | const firebaseErrorMapping: FirebaseErrorMapping = { 14 | "auth/invalid-login-credentials": 15 | "Invalid login credentials. Please check your email and password and try again.", 16 | "auth/user-not-found": 17 | "No account found with this email. Please sign up if you're new.", 18 | "auth/wrong-password": 19 | "Incorrect password. Please try again or reset your password.", 20 | "auth/weak-password": 21 | "Password should be at least 6 characters long. Please choose a stronger password.", 22 | "auth/email-already-in-use": 23 | "This email address is already in use by another account. Please use a different email address or sign in to your existing account.", 24 | }; 25 | 26 | function getFriendlyErrorMessage(error: FirebaseError): string { 27 | return firebaseErrorMapping[error.code] || error.message; 28 | } 29 | 30 | export default getFriendlyErrorMessage; 31 | -------------------------------------------------------------------------------- /src/components/Navbar.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
26 |
29 | 30 | Astro logo 37 |

Astro

38 |
39 |
40 | {#if user} 41 | 42 | {:else} 43 | 44 | {/if} 45 |
46 |
47 |
48 | -------------------------------------------------------------------------------- /src/firebase/utils/auth/handleSignup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Handles the user signup process using Firebase authentication. It registers the 3 | * user with an email and password, updates the user profile with a display name, 4 | * and sets up a session with the server using the ID token received from Firebase. 5 | * 6 | * @param {string} name - The display name of the user. 7 | * @param {string} email - The email address of the user to register. 8 | * @param {string} password - The password for the user account. 9 | * @throws {Error} If registration, profile update, or server session creation fails. 10 | */ 11 | import { 12 | createUserWithEmailAndPassword, 13 | getAuth, 14 | inMemoryPersistence, 15 | updateProfile, 16 | } from "firebase/auth"; 17 | import { app } from "@/firebase/client"; 18 | import fetchSignIn from "../../../services/api/fetchSignIn"; 19 | 20 | const auth = getAuth(app); 21 | auth.setPersistence(inMemoryPersistence); 22 | 23 | async function handleSignup(name: string, email: string, password: string) { 24 | // Register the user using Firebase's client SDK 25 | const userCredential = await createUserWithEmailAndPassword( 26 | auth, 27 | email, 28 | password 29 | ); 30 | // Update the user's profile with the provided name 31 | await updateProfile(userCredential.user, { 32 | displayName: name, 33 | }); 34 | 35 | // Get the ID token of the newly registered user 36 | const idToken = await userCredential.user.getIdToken(); 37 | 38 | const redirectedUrl = await fetchSignIn(idToken); 39 | 40 | return redirectedUrl; 41 | } 42 | 43 | export default handleSignup; 44 | -------------------------------------------------------------------------------- /src/components/UserDropdown.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#if user?.email} 11 | 12 | 24 |
25 |
26 | {#if user?.displayName} 27 |

28 | {user?.displayName} 29 |

30 | {/if} 31 |

32 | {user?.email} 33 |

34 |
35 | 39 | 40 |
41 | 45 |
46 |
47 |
48 | {/if} 49 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Service Account to `.env` Converter 2 | 3 | This directory contains a utility script named `createEnvFromServiceAccount.js`. The purpose of this script is to automate the conversion of Firebase service account credentials from a JSON file to a `.env` file format. 4 | 5 | ## Why Is This Needed? 6 | 7 | This is particularly useful for setting up your project's environment variables without manually copying and pasting each key-value pair. 8 | 9 | ## Usage Instructions 10 | 11 | To convert your service account credentials to a `.env` file: 12 | 13 | 1. Obtain your `service-account.json` file from the Firebase Console by navigating to `Project settings > Service accounts > Generate new private key`. 14 | 2. Save this file in the root directory of your project. 15 | 3. Execute the script with Node.js by running: 16 | 17 | ```bash 18 | node scripts/createEnvFromServiceAccount.js` 19 | 20 | ``` 21 | 22 | 4. Upon successful execution, a `.env` file will be created in the root of your project directory with all the required Firebase environment variables set up. 23 | 24 | ## Prerequisites 25 | 26 | - You must have Node.js installed on your machine to run this script. 27 | - The `service-account.json` file must be present and contain your Firebase service account credentials. 28 | 29 | 30 | ## Important Notes 31 | 32 | - The generated `.env` and `service-account.json` file should **never be committed** to version control if your repository and the server is public. Always add `.env` and `service-account.json` to your `.gitignore` file to prevent sensitive credentials from being exposed. 33 | - Ensure that any deployment workflows or hosting environments are configured to secure your `.env` file or its contents appropriately. 34 | 35 | Thank you for using this utility to streamline your Firebase project configuration. 36 | -------------------------------------------------------------------------------- /src/components/Home.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
16 |
17 | {#if user} 18 | 19 |
20 |
21 | 26 |
27 | 30 |
31 |
32 |
33 | {:else} 34 | 35 |
36 |
37 | 44 | 47 |
48 |
49 | {/if} 50 |
51 |
52 | -------------------------------------------------------------------------------- /public/astro.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | astro 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/pages/api/user/update.ts: -------------------------------------------------------------------------------- 1 | import type { APIRoute } from "astro"; 2 | import updateUserData from "@/firebase/utils/auth/updateUserData"; 3 | import getUserData from "@/firebase/utils/auth/getUserData"; 4 | 5 | export const POST: APIRoute = async ({ request, cookies }) => { 6 | try { 7 | const sessionCookie = cookies.get("session")?.value ?? null; 8 | // Get user data based on the session cookie. 9 | const user = await getUserData(sessionCookie); 10 | 11 | if (!user) { 12 | return new Response(JSON.stringify({ error: "Unauthorized" }), { 13 | status: 401, 14 | headers: { "Content-Type": "application/json" }, 15 | }); 16 | } 17 | 18 | // User is authenticated, proceed with updating user data... 19 | // Extract the data you want to update from the request body. 20 | const updates = await request.json(); 21 | if (!updates) { 22 | return new Response( 23 | JSON.stringify({ error: "Error: No Updates Passed" }), 24 | { 25 | status: 404, 26 | headers: { "Content-Type": "application/json" }, 27 | } 28 | ); 29 | } 30 | const updatedUserRecord = await updateUserData(user.uid, updates); 31 | // Return success response 32 | return new Response( 33 | JSON.stringify({ success: true, data: updatedUserRecord }), 34 | { 35 | status: 200, 36 | headers: { "Content-Type": "application/json" }, 37 | } 38 | ); 39 | } catch (error) { 40 | // Log the error for server-side debugging. 41 | console.error(error); 42 | let errorMessage = "Internal Server Error"; 43 | let statusCode = 500; 44 | 45 | // Check if error is an instance of Error 46 | if (error instanceof Error) { 47 | errorMessage = error.message; 48 | // If the error object has a 'statusCode' property, use it. 49 | statusCode = (error as { statusCode?: number }).statusCode || 500; 50 | } 51 | 52 | // Return a generic error response or customize based on the error type 53 | return new Response(JSON.stringify({ error: errorMessage }), { 54 | status: statusCode, 55 | headers: { "Content-Type": "application/json" }, 56 | }); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/base/popover/Popover.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 47 | 48 |
49 | 50 | 51 | 52 | {#if open} 53 | 66 | {/if} 67 |
68 | -------------------------------------------------------------------------------- /src/components/ForgetPasswordForm.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 |
28 |
29 |
30 |

31 | Reset Password 32 |

33 |

34 | Enter the email associated with your account. 35 |

36 |
37 |
38 |
39 |
40 | 41 | 48 |
49 |
50 | {#if errorMessage} 51 |
52 |

{errorMessage}

53 |
54 | {/if} 55 | {#if successMessage} 56 |
57 |

{successMessage}

58 |
59 | {/if} 60 |
61 | {#if loading} 62 | 63 | {:else} 64 | 68 | {/if} 69 |
70 |
71 |
72 |
73 | -------------------------------------------------------------------------------- /src/components/base/button/Button.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 36 | {#if isLink()} 37 | 46 | 47 | 48 | {:else} 49 | 57 | {/if} 58 | -------------------------------------------------------------------------------- /src/components/ConfirmPasswordForm.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | 34 |
37 |
38 |
39 |

40 | Reset Your Password 41 |

42 |

43 | Enter and confirm your new password below. 44 |

45 |
46 | {#if errorMessage} 47 |
48 |

{errorMessage}

49 |
50 | {/if} 51 | {#if successMessage} 52 |
53 |

{successMessage}

54 |
55 | {/if} 56 |
60 |
61 |
62 | 63 | 70 |
71 |
72 | 73 | 80 |
81 |
82 |
83 | 84 |
85 |
86 |
87 |
88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Screenshot of the Astro Firebase Svelte Tailwind starter's Banner showing a clean and responsive design sign in and a perfect google lighthouse scores with 100% in Performance, Accessibility, Best Practices, and SEO 3 | 4 |

Astro Firebase Starter

5 | 6 |

7 | A starter pack for getting started with Tailwind, Astro, Firebase, and Svelte. 8 |

9 | 10 |

11 | Try me! 12 |

13 | 14 | ## Getting Started 15 | 16 | To use this starter pack, follow these steps: 17 | 18 | 1. **Clone the repository:** 19 | 20 | ```bash 21 | git clone https://github.com/Porter-smith/astro-firebase-svelte-tailwind-starter.git 22 | ``` 23 | 24 | 2. **Navigate to the project directory:** 25 | 26 | ```bash 27 | cd astro-firebase-svelte-tailwind-starter 28 | ``` 29 | 30 | 3. **Install the dependencies:** 31 | 32 | ```bash 33 | pnpm install 34 | ``` 35 | 36 | 4. **Start the development server:** 37 | 38 | ```bash 39 | pnpm run dev 40 | ``` 41 | 42 | # Quickstart 43 | 44 | Follow this guide to quickly set up Firebase for your project. For more in-depth instructions, see [FIREBASE_SETUP.md](./docs/FIREBASE_SETUP.md). 45 | 46 | ## Obtain Firebase Keys 47 | 48 | ### Service Account Key 49 | 50 | 1. Go to the [Firebase Console](https://console.firebase.google.com/). 51 | 2. Navigate to `Project settings` > `Service accounts`. 52 | 3. Click `Generate new private key`, then download and secure the JSON file. 53 | 54 | ### Web App Configuration 55 | 56 | 1. If you haven't added a web app to [Firebase](https://console.firebase.google.com/), click the web icon (``) in `Project settings` > `Your apps` to create one. 57 | 2. Find your web app and you should see your `configuration` 58 | 3. Copy the configuration object. 59 | 60 | ## Update Configuration Files 61 | 62 | Place your keys in the appropriate files: 63 | 64 | - `.env`: Add service account key values from the downloaded JSON. 65 | - `src/firebase/client.ts`: Insert the web app configuration object. 66 | 67 | ### Set up .env with Helper Script 68 | 69 | To configure your environment variables quickly: 70 | 71 | Save your service account creds as `service-account.json` in the project's root. 72 | Run 73 | 74 | ```bash 75 | node scripts/createEnvFromServiceAccount.js 76 | ``` 77 | 78 | A .env file with all Firebase variables will be generated. So you don't have to copy and paste one by one each key. 79 | 80 | **Note:** After setting up the .env file, remember to remove the service-account.json from your project's root directory to protect your credentials. 81 | 82 | ## Enable Authentication 83 | 84 | ### Configure Auth Methods 85 | 86 | 1. In [Firebase Console](https://console.firebase.google.com/), go to `Authentication`. 87 | 2. Click `Get started`. 88 | 3. Enable your desired sign-in methods and configure each according to Firebase's prompts. 89 | 90 | ## Ready to Go 91 | 92 | You should now be able to run your application and use Firebase's authentication services. 93 | 94 | 🚀 Happy coding! 95 | -------------------------------------------------------------------------------- /src/components/SignupForm.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 |
24 |
25 |
26 |

27 | Sign up 28 |

29 |

30 | Already have an account? 31 | 32 | Sign in 33 | 34 |

35 |
36 |
37 |
38 |
39 | 40 | 47 |
48 |
49 | 50 | 57 |
58 |
59 | 60 | 67 |
68 |
69 | {#if errorMessage} 70 | 71 |
72 |

{errorMessage}

73 |
74 | {/if} 75 |
76 | 77 |
78 |
79 |
80 |
81 | 82 |
83 |
84 | Or continue with 85 |
86 |
87 |
88 | 89 |
90 |
91 |
92 | -------------------------------------------------------------------------------- /scripts/createEnvFromServiceAccount.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import readline from 'readline'; 3 | 4 | const rl = readline.createInterface({ 5 | input: process.stdin, 6 | output: process.stdout 7 | }); 8 | 9 | async function checkFileExists(filePath) { 10 | try { 11 | await fs.access(filePath); 12 | return true; 13 | } catch { 14 | return false; 15 | } 16 | } 17 | 18 | function askQuestion(question) { 19 | return new Promise((resolve) => { 20 | // Added a newline character before the question to have it on a new line 21 | rl.question(`\n${question} (Y/n) `, (input) => { 22 | resolve(input.toLowerCase() === 'n' ? 'n' : 'y'); 23 | }); 24 | }); 25 | } 26 | 27 | async function confirmAction(question, defaultYes = true) { 28 | const response = await askQuestion(question); 29 | return defaultYes ? response !== 'n' : response === 'y'; 30 | } 31 | 32 | async function readAndParseJsonFile(filePath) { 33 | const data = await fs.readFile(filePath, 'utf8'); 34 | return JSON.parse(data); 35 | } 36 | 37 | function constructEnvContent(serviceAccount) { 38 | const escapedPrivateKey = serviceAccount.private_key.replace(/\n/g, '\\n'); 39 | return `FIREBASE_PRIVATE_KEY_ID=${serviceAccount.private_key_id} 40 | FIREBASE_PRIVATE_KEY="${escapedPrivateKey}" 41 | FIREBASE_PROJECT_ID=${serviceAccount.project_id} 42 | FIREBASE_CLIENT_EMAIL=${serviceAccount.client_email} 43 | FIREBASE_CLIENT_ID=${serviceAccount.client_id} 44 | FIREBASE_AUTH_URI=${serviceAccount.auth_uri} 45 | FIREBASE_TOKEN_URI=${serviceAccount.token_uri} 46 | FIREBASE_AUTH_PROVIDER_CERT_URL=${serviceAccount.auth_provider_x509_cert_url} 47 | FIREBASE_CLIENT_CERT_URL=${serviceAccount.client_x509_cert_url}`; 48 | } 49 | 50 | async function deleteFile(filePath) { 51 | await fs.unlink(filePath); 52 | } 53 | 54 | async function convertServiceAccountToJson() { 55 | if (!await checkFileExists('service-account.json')) { 56 | console.error('The `service-account.json` file does not exist. Please make sure it is in the root directory of your project.'); 57 | rl.close(); 58 | return; 59 | } 60 | 61 | if (!await confirmAction('Start the conversion of service-account.json to .env?\nContinue?')) { 62 | console.log('Conversion process aborted.'); 63 | rl.close(); 64 | return; 65 | } 66 | 67 | try { 68 | const serviceAccount = await readAndParseJsonFile('service-account.json'); 69 | const envContent = constructEnvContent(serviceAccount); 70 | await fs.writeFile('.env', envContent); 71 | console.log('.env file created successfully!'); 72 | 73 | if (await confirmAction('The .env file has been successfully created from `service-account.json`.\nWould you like to remove the service-account.json file now?\nContinue?')) { 74 | await deleteFile('service-account.json'); 75 | console.log('The service-account.json file has been deleted successfully.'); 76 | } else { 77 | console.log('The service-account.json file has not been deleted. For security purposes, please delete it manually after ensuring the .env file contains the correct information.'); 78 | 79 | } 80 | } catch (err) { 81 | console.error('Error:', err); 82 | } finally { 83 | rl.close(); 84 | } 85 | } 86 | 87 | convertServiceAccountToJson(); 88 | -------------------------------------------------------------------------------- /src/components/EditAccountForm.svelte: -------------------------------------------------------------------------------- 1 | 47 | 48 |
51 |
52 |
53 |

54 | Edit Account 55 |

56 |
57 |
61 |
62 |
63 | 64 | 71 |
72 |
73 | 74 | 81 |
82 |
83 | {#if successMessage} 84 |
85 |

{successMessage}

86 |
87 | {/if} 88 | {#if errorMessage} 89 |
90 |

{errorMessage}

91 |
92 | {/if} 93 |
94 | {#if loading} 95 | 96 | {:else} 97 | 100 | {/if} 101 |
102 |
103 |
104 |
105 | -------------------------------------------------------------------------------- /src/components/SigninForm.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 |
42 |
43 |
44 |

45 | 46 | Sign in 47 |

48 |

49 | New? 50 | 51 | Create an account 52 | 53 |

54 |
55 |
56 |
57 |
58 | 59 | 66 |
67 |
68 | 69 | 76 | 84 |
85 |
86 | {#if errorMessage} 87 | 88 |
89 |

{errorMessage}

90 |
91 | {/if} 92 | 93 |
94 | {#if loading} 95 | 96 | {:else} 97 | 98 | {/if} 99 |
100 |
101 | 102 |
103 |
104 | 105 |
106 |
107 | Or continue with 108 |
109 |
110 | 111 |
112 | 113 |
114 |
115 |
116 | -------------------------------------------------------------------------------- /docs/FIREBASE_SETUP.md: -------------------------------------------------------------------------------- 1 | ## Firebase Configuration 2 | 3 | To integrate your Firebase project with this starter pack, you will need to set up your Firebase configuration. Follow these steps: 4 | 5 | ### Client-side Firebase API keys: 6 | 7 | 1. Obtain your web app's Firebase configuration keys from the [Firebase Console](https://console.firebase.google.com/). 8 | - Navigate to `Project settings` > `General` > `Your apps`. 9 | - If you haven't added a web app to your project yet, click the web icon (``) and follow the prompts. 10 | 11 | ### Incorporate Client-side Firebase API keys into your project: 12 | 13 | 1. Within your project, navigate to `src/firebase/`. 14 | 2. Create or edit `client.ts` and insert your Firebase configuration: 15 | 16 | ```typescript 17 | // src/firebase/client.ts 18 | 19 | import { initializeApp } from "firebase/app"; 20 | 21 | // Replace below with your app's Firebase configuration from the Firebase console 22 | const firebaseConfig = { 23 | apiKey: "your_api_key", 24 | authDomain: "your_project_id.firebaseapp.com", 25 | projectId: "your_project_id", 26 | storageBucket: "your_project_id.appspot.com", 27 | messagingSenderId: "your_messaging_sender_id", 28 | appId: "your_app_id", 29 | measurementId: "your_measurement_id", 30 | }; 31 | 32 | // Initialize Firebase 33 | export const app = initializeApp(firebaseConfig); 34 | ``` 35 | 36 | ### Server-side Firebase Configuration 37 | 38 | From the [Firebase Console](https://console.firebase.google.com/), navigate to `Project settings` > `Service accounts` and generate a new private key. 39 | 40 | ## Set up Environment Variables 41 | 42 | ### Automated Setup with Helper Script 43 | 44 | For a quick setup, use our `createEnvFromServiceAccount.js` script to convert your service account JSON into a `.env` file: 45 | 46 | 1. Place your service account JSON file in the project root and rename it to `service-account.json`. 47 | 2. Run the script in your terminal: 48 | 49 | ```bash 50 | node scripts/createEnvFromServiceAccount.js 51 | ``` 52 | 53 | ### Manual Setup Option 54 | 55 | If you prefer to manually configure the environment variables: 56 | 57 | 1. Open `service-account.json` and manually extract the necessary fields. 58 | 2. Create a new `.env` file in your project root. 59 | 3. Use the structure below for your `.env` file, replacing each placeholder with the corresponding value from your JSON file: 60 | 61 | ```env 62 | FIREBASE_PRIVATE_KEY_ID= 63 | FIREBASE_PRIVATE_KEY="" 64 | FIREBASE_PROJECT_ID= 65 | FIREBASE_CLIENT_EMAIL= 66 | FIREBASE_CLIENT_ID= 67 | FIREBASE_AUTH_URI= 68 | FIREBASE_TOKEN_URI= 69 | FIREBASE_AUTH_PROVIDER_CERT_URL= 70 | FIREBASE_CLIENT_CERT_URL= 71 | ``` 72 | 73 | 🎉 All set! You're ready to build with Firebase integrated into your project. 74 | 75 | ## Integrating Firebase Features 76 | 77 | Now that you have set up your Firebase environment, you can start integrating various Firebase services such as Firestore and authentication methods like Google Sign-In and Email/Password authentication. 78 | 79 | ### Enabling Firestore Database 80 | 81 | 1. Go to the [Firebase Console](https://console.firebase.google.com/). 82 | 2. Select your project and navigate to `Firestore Database` in the left menu. 83 | 3. Click `Create database` and follow the prompts to set up Firestore. For a safer environment that enforces your security rules, I recommend starting in `production mode`. 84 | 85 | ### Adding Authentication Methods 86 | 87 | #### Google Sign-In 88 | 89 | 1. In the Firebase Console, navigate to `Authentication`. 90 | 2. If you haven’t already set up authentication, click `Get started`. 91 | 3. Then, go to the `Sign-in method` tab. 92 | 4. Click on `Google` and toggle the enable switch to activate Google as a sign-in provider. 93 | 5. Configure your OAuth consent screen and save the Web client ID as prompted. 94 | 95 | > **Important**: Adding your domain to the on `Authentication` > `Authorized Domains` for Google Sign-In is essential once your application is deployed on not on localhost. This step is crucial for Google Sign-In to work on your deployed site. 96 | 97 | #### Email/Password Authentication 98 | 99 | 1. Still in the `Authentication` section, locate `Email/Password` and toggle it on. 100 | 2. Note that `Email link (passwordless sign-in)` is not supported by the template. 101 | 102 | ### Resources and Support 103 | 104 | - Refer to the [official Firebase documentation](https://firebase.google.com/docs) for in-depth guides and API references. 105 | -------------------------------------------------------------------------------- /src/lib/actions.ts: -------------------------------------------------------------------------------- 1 | import type { ActionReturn } from "svelte/action"; 2 | // https://github.com/WailAbou/shadcn-svelte-nodep/blob/72b96d937afd3b79ae04f883e71e6b42daddb5d7/src/lib/helpers/actions.ts#L5 3 | // Popover 4 | /** 5 | * `clickOutside` is an action that enhances UI interaction for popovers and modals. It monitors for clicks outside of the 6 | * target element and then executes a callback, typically used to close the UI element. 7 | * Arguments: 8 | * - `node`: The HTMLElement to which clickOutside is applied. 9 | * - `callback`: The function to call when a click outside is detected. 10 | * - `except`: An optional HTMLElement to ignore in the outside click detection, useful for elements like popover triggers. 11 | */ 12 | 13 | export function clickOutside( 14 | node: Node, 15 | [callback, except]: [VoidFunction, HTMLElement?] 16 | ): ActionReturn<[VoidFunction, HTMLElement?]> { 17 | const onClick = (event: MouseEvent) => { 18 | const target = event.target as Node; 19 | if (!node.contains(target) && (!except || !except.contains(target))) { 20 | callback(); 21 | } 22 | }; 23 | 24 | document.body.addEventListener("click", onClick); 25 | 26 | return { 27 | update([newCallback, newExcept]: [VoidFunction, HTMLElement?]) { 28 | callback = newCallback; 29 | except = newExcept; 30 | }, 31 | destroy() { 32 | document.body.removeEventListener("click", onClick); 33 | }, 34 | }; 35 | } 36 | 37 | // https://github.com/WailAbou/shadcn-svelte-nodep/blob/72b96d937afd3b79ae04f883e71e6b42daddb5d7/src/lib/helpers/actions.ts#L26 38 | // Popover 39 | /** 40 | * `keyDown` action enhances keyboard navigation within popovers/modals by listening for specific key events, such as pressing the 'ESC' key. 41 | * When activated, it invokes a callback, often used to close the popover, ensuring a keyboard-friendly interface, per WCAG 2.1 accessibility standards. 42 | * Arguments: 43 | * - `condition`: Boolean to enable or disable the keydown listener. 44 | * - `callback`: Function to execute when the specified keys are pressed. 45 | * - `codes`: Array of key codes to listen for, e.g., ['Escape'] for the 'ESC' key. 46 | * - `shiftKey`: KeyCombination to consider the state of the 'Shift' key, with possible values 'ignore', 'always', 'never'. 47 | */ 48 | type KeyCombination = "always" | "never" | "ignore"; 49 | export function keyDown( 50 | node: Node, 51 | [condition, callback, codes, shiftKey = "ignore"]: [ 52 | boolean, 53 | VoidFunction, 54 | string[], 55 | KeyCombination? 56 | ] 57 | ): ActionReturn { 58 | const onKeyDown: EventListener = (event: Event) => { 59 | const e = event as KeyboardEvent; 60 | 61 | const shiftCondition = 62 | shiftKey === "ignore" || 63 | (shiftKey === "never" && !e.shiftKey) || 64 | (shiftKey === "always" && e.shiftKey); 65 | 66 | if (condition && codes.includes(e.code) && shiftCondition) { 67 | e.preventDefault(); 68 | callback(); 69 | } 70 | }; 71 | 72 | node.addEventListener("keydown", onKeyDown); 73 | 74 | return { 75 | destroy() { 76 | node.removeEventListener("keydown", onKeyDown); 77 | }, 78 | }; 79 | } 80 | // Source: https://github.com/WailAbou/shadcn-svelte-nodep/blob/72b96d937afd3b79ae04f883e71e6b42daddb5d7/src/lib/helpers/actions.ts#L76 81 | // Popover 82 | /** 83 | * `focusTrap` is an action designed to maintain accessibility within a modal or popover by trapping keyboard focus. 84 | * It prevents focus from leaving the modal, allowing users to cycle through focusable elements using Tab for forward 85 | * and Shift+Tab for reverse navigation. This functionality is essential for users with assistive technologies, complying with WCAG guidelines. 86 | * 87 | * Arguments: 88 | * - `node`: The HTMLElement to which the focus trap is applied. 89 | * - `enabled`: Boolean that enables or disables the focus trap. 90 | */ 91 | export function focusTrap(node: HTMLElement, enabled: boolean = true) { 92 | const elemWhitelist: string[] = [ 93 | "a[href]", 94 | "area[href]", 95 | 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])', 96 | "select:not([disabled]):not([aria-hidden])", 97 | "textarea:not([disabled]):not([aria-hidden])", 98 | "button:not([disabled]):not([aria-hidden])", 99 | "iframe", 100 | "object", 101 | "embed", 102 | "[contenteditable]", 103 | '[tabindex]:not([tabindex^="-"])', 104 | ]; 105 | let elemFirst: HTMLElement; 106 | let elemLast: HTMLElement; 107 | 108 | function onFirstElemKeydown(e: KeyboardEvent): void { 109 | if (e.shiftKey && e.code === "Tab") { 110 | e.preventDefault(); 111 | elemLast.focus(); 112 | } 113 | } 114 | function onLastElemKeydown(e: KeyboardEvent): void { 115 | if (!e.shiftKey && e.code === "Tab") { 116 | e.preventDefault(); 117 | elemFirst.focus(); 118 | } 119 | } 120 | 121 | const onScanElements = (fromObserver: boolean) => { 122 | if (enabled === false) return; 123 | 124 | const focusableElems: HTMLElement[] = Array.from( 125 | node.querySelectorAll(elemWhitelist.join(", ")) 126 | ); 127 | if (focusableElems.length) { 128 | elemFirst = focusableElems[0]; 129 | elemLast = focusableElems[focusableElems.length - 1]; 130 | 131 | if (!fromObserver) elemFirst.focus(); 132 | 133 | elemFirst.addEventListener("keydown", onFirstElemKeydown); 134 | elemLast.addEventListener("keydown", onLastElemKeydown); 135 | } 136 | }; 137 | onScanElements(false); 138 | 139 | function onCleanUp(): void { 140 | if (elemFirst) elemFirst.removeEventListener("keydown", onFirstElemKeydown); 141 | if (elemLast) elemLast.removeEventListener("keydown", onLastElemKeydown); 142 | } 143 | 144 | const onObservationChange = ( 145 | mutationRecords: MutationRecord[], 146 | observer: MutationObserver 147 | ) => { 148 | if (mutationRecords.length) { 149 | onCleanUp(); 150 | onScanElements(true); 151 | } 152 | return observer; 153 | }; 154 | const observer = new MutationObserver(onObservationChange); 155 | observer.observe(node, { childList: true, subtree: true }); 156 | 157 | return { 158 | update(newArgs: boolean) { 159 | enabled = newArgs; 160 | newArgs ? onScanElements(false) : onCleanUp(); 161 | }, 162 | destroy() { 163 | onCleanUp(); 164 | observer.disconnect(); 165 | }, 166 | }; 167 | } 168 | --------------------------------------------------------------------------------