├── .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 |
10 |
11 | Please wait
12 |
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 |
18 | {#if loading}
19 |
20 | Please wait
21 | {:else}
22 |
23 | Sign in with Google
24 | {/if}
25 |
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 | Edit Your Account
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 |
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 |
18 | {#if user?.photoURL}
19 |
20 | {:else}
21 |
22 | {/if}
23 |
24 |
25 |
26 | {#if user?.displayName}
27 |
28 | {user?.displayName}
29 |
30 | {/if}
31 |
32 | {user?.email}
33 |
34 |
35 |
36 |
37 | Dashboard
38 |
39 |
40 |
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 | Go to Dashboard
26 |
31 |
32 |
33 | {:else}
34 |
35 |
36 |
37 |
42 | Sign in
43 |
44 |
45 | Sign up
46 |
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 |
63 |
64 |
65 |
66 | {/if}
67 |
68 |
--------------------------------------------------------------------------------
/src/components/ForgetPasswordForm.svelte:
--------------------------------------------------------------------------------
1 |
24 |
25 |
28 |
29 |
30 |
33 |
34 | Enter the email associated with your account.
35 |
36 |
37 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/src/components/base/button/Button.svelte:
--------------------------------------------------------------------------------
1 |
27 |
28 |
36 | {#if isLink()}
37 |
46 |
47 |
48 | {:else}
49 |
55 |
56 |
57 | {/if}
58 |
--------------------------------------------------------------------------------
/src/components/ConfirmPasswordForm.svelte:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
34 |
37 |
38 |
39 |
42 |
43 | Enter and confirm your new password below.
44 |
45 |
46 | {#if errorMessage}
47 |
50 | {/if}
51 | {#if successMessage}
52 |
53 |
{successMessage}
54 |
55 | {/if}
56 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
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 |
36 |
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 |
56 |
57 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/src/components/SigninForm.svelte:
--------------------------------------------------------------------------------
1 |
38 |
39 |
42 |
43 |
55 |
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 |
--------------------------------------------------------------------------------