Skip to content

Commit 2f4ebb2

Browse files
authored
Merge pull request #2167 from codeforboston/main
Deploy to PROD 6/16/26
2 parents ef28e28 + b7ddd9d commit 2f4ebb2

233 files changed

Lines changed: 11672 additions & 3088 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,8 @@ cert.txt
8787

8888
# lets each user define their own vscode settings
8989
.vscode/settings.json
90+
91+
.serena/
92+
# local MCP server config (contains auth tokens)
93+
.mcp.json
94+
mcp-server/create-agent-key.ts

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
20

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ coverage
1515
storybook-static
1616
llm
1717
playwright-report
18+
CLAUDE.md
19+
.cursor/
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
const authModule = require("@firebase/auth")
2+
const {
3+
getStorybookAuthState,
4+
setStorybookAuthState,
5+
shouldAllowFirebaseCall,
6+
throwBlockedFirebaseCall
7+
} = require("./common")
8+
9+
function emitAuthState(callback) {
10+
const { user } = getStorybookAuthState()
11+
callback(user)
12+
}
13+
14+
function block(apiName) {
15+
return () => {
16+
throwBlockedFirebaseCall(
17+
"auth",
18+
apiName,
19+
"Mock auth state in your story providers or pass explicit props that avoid auth hooks."
20+
)
21+
}
22+
}
23+
24+
function onAuthStateChanged(auth, nextOrObserver, error, completed) {
25+
if (shouldAllowFirebaseCall()) {
26+
return authModule.onAuthStateChanged(auth, nextOrObserver, error, completed)
27+
}
28+
29+
const callback =
30+
typeof nextOrObserver === "function"
31+
? nextOrObserver
32+
: nextOrObserver?.next ?? (() => undefined)
33+
34+
emitAuthState(callback)
35+
36+
return () => undefined
37+
}
38+
39+
function patchAuthInstance(auth) {
40+
if (!auth || shouldAllowFirebaseCall()) return auth
41+
42+
return new Proxy(auth, {
43+
get(target, prop, receiver) {
44+
if (prop === "onAuthStateChanged") {
45+
return onAuthStateChanged
46+
}
47+
return Reflect.get(target, prop, receiver)
48+
}
49+
})
50+
}
51+
52+
function getAuth(...args) {
53+
const auth = authModule.getAuth(...args)
54+
return patchAuthInstance(auth)
55+
}
56+
57+
function initializeAuth(...args) {
58+
const auth = authModule.initializeAuth(...args)
59+
return patchAuthInstance(auth)
60+
}
61+
62+
module.exports = {
63+
...authModule,
64+
getAuth,
65+
initializeAuth,
66+
onAuthStateChanged,
67+
signInWithEmailAndPassword: block("signInWithEmailAndPassword"),
68+
signInWithPopup: block("signInWithPopup"),
69+
signInWithRedirect: block("signInWithRedirect"),
70+
signInAnonymously: block("signInAnonymously"),
71+
mockLoggedOutAuthState() {
72+
return setStorybookAuthState({ user: null })
73+
},
74+
mockLoggedInUserAuthState(overrides = {}) {
75+
const user = {
76+
uid: "storybook-user",
77+
email: "storybook-user@example.com",
78+
emailVerified: true,
79+
displayName: "Storybook User",
80+
isAnonymous: false,
81+
providerId: "firebase",
82+
photoURL: null,
83+
phoneNumber: null,
84+
tenantId: null,
85+
metadata: {
86+
creationTime: "",
87+
lastSignInTime: ""
88+
},
89+
providerData: [],
90+
refreshToken: "storybook-refresh-token",
91+
stsTokenManager: {
92+
accessToken: "storybook-access-token",
93+
refreshToken: "storybook-refresh-token",
94+
expirationTime: Date.now() + 60 * 60 * 1000
95+
},
96+
getIdToken: async () => "storybook-id-token",
97+
getIdTokenResult: async () => ({
98+
token: "storybook-id-token",
99+
authTime: "",
100+
issuedAtTime: "",
101+
expirationTime: "",
102+
signInProvider: null,
103+
claims: {
104+
role: "user",
105+
email_verified: true
106+
}
107+
}),
108+
reload: async () => undefined,
109+
delete: async () => undefined,
110+
toJSON: () => ({})
111+
}
112+
113+
return setStorybookAuthState({
114+
user: { ...user, ...overrides },
115+
claims: { role: "user", email_verified: true }
116+
})
117+
}
118+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
const ALLOW_FLAG = "__MAPLE_STORYBOOK_ALLOW_FIREBASE__"
2+
const WARNED_FLAG = "__MAPLE_STORYBOOK_FIREBASE_WARNED__"
3+
const AUTH_STATE_KEY = "__MAPLE_STORYBOOK_FIREBASE_AUTH_STATE__"
4+
5+
function canAllowInRuntime() {
6+
if (typeof globalThis !== "undefined") {
7+
return Boolean(globalThis[ALLOW_FLAG])
8+
}
9+
return false
10+
}
11+
12+
function canAllowFromEnv() {
13+
return process.env.STORYBOOK_ALLOW_FIREBASE_CALLS === "1"
14+
}
15+
16+
function shouldAllowFirebaseCall() {
17+
return canAllowFromEnv() || canAllowInRuntime()
18+
}
19+
20+
function warnOnce(key, message) {
21+
if (typeof globalThis === "undefined") return
22+
23+
if (!globalThis[WARNED_FLAG]) {
24+
globalThis[WARNED_FLAG] = new Set()
25+
}
26+
27+
const warned = globalThis[WARNED_FLAG]
28+
if (!warned.has(key)) {
29+
warned.add(key)
30+
console.warn(message)
31+
}
32+
}
33+
34+
function throwBlockedFirebaseCall(service, apiName, helpText) {
35+
if (shouldAllowFirebaseCall()) return
36+
37+
const message = [
38+
`[Storybook Firebase Guard] Blocked ${service} call: ${apiName}`,
39+
"Real Firebase calls are disabled by default in Storybook.",
40+
"Mock the component data/hooks used by this story instead.",
41+
helpText,
42+
"To opt out temporarily for a specific story, set: parameters.firebaseGuard.allow = true",
43+
"To opt out globally, start Storybook with STORYBOOK_ALLOW_FIREBASE_CALLS=1"
44+
].join("\n")
45+
46+
warnOnce(`${service}:${apiName}`, message)
47+
throw new Error(message)
48+
}
49+
50+
function getStorybookAuthState() {
51+
if (typeof globalThis === "undefined") return { user: null }
52+
53+
return globalThis[AUTH_STATE_KEY] ?? { user: null }
54+
}
55+
56+
function setStorybookAuthState(state) {
57+
if (typeof globalThis !== "undefined") {
58+
globalThis[AUTH_STATE_KEY] = state
59+
}
60+
return state
61+
}
62+
63+
module.exports = {
64+
getStorybookAuthState,
65+
setStorybookAuthState,
66+
shouldAllowFirebaseCall,
67+
throwBlockedFirebaseCall
68+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const firestore = require("@firebase/firestore")
2+
const { throwBlockedFirebaseCall } = require("./common")
3+
4+
function block(apiName) {
5+
return () => {
6+
throwBlockedFirebaseCall(
7+
"firestore",
8+
apiName,
9+
"Provide mocked query results or inject mock props into the component under test."
10+
)
11+
}
12+
}
13+
14+
module.exports = {
15+
...firestore,
16+
getDoc: block("getDoc"),
17+
getDocs: block("getDocs"),
18+
onSnapshot: block("onSnapshot"),
19+
addDoc: block("addDoc"),
20+
setDoc: block("setDoc"),
21+
updateDoc: block("updateDoc"),
22+
deleteDoc: block("deleteDoc"),
23+
runTransaction: block("runTransaction"),
24+
getCountFromServer: block("getCountFromServer")
25+
}

.storybook/main.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/**
22
* @type {import('@storybook/react/types').StorybookConfig}
33
*/
4+
const path = require("path")
5+
46
module.exports = {
57
stories: [
68
"../stories/**/*.stories.mdx",
@@ -24,6 +26,28 @@ module.exports = {
2426
use: ["file-loader"]
2527
})
2628
config.resolve.fallback = { fs: false, path: false }
29+
30+
const path = require("path")
31+
const webpack = require("webpack")
32+
config.plugins.push(
33+
new webpack.NormalModuleReplacementPlugin(
34+
/components\/db\/profile\/profile(\.tsx?)?$/,
35+
path.resolve(__dirname, "../stories/__mocks__/db/profile.ts")
36+
)
37+
)
38+
39+
config.resolve.alias = {
40+
...(config.resolve.alias || {}),
41+
"firebase/firestore$": path.resolve(
42+
__dirname,
43+
"./firebase-guards/firestore.guard.js"
44+
),
45+
"firebase/auth$": path.resolve(
46+
__dirname,
47+
"./firebase-guards/auth.guard.js"
48+
)
49+
}
50+
2751
return config
2852
},
2953

.storybook/preview.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import React, { Suspense } from "react"
99
import { I18nextProvider } from "react-i18next"
1010
// import i18n from "i18next"
1111
import i18n from "./i18n"
12+
const { mockLoggedOutAuthState } = require("./firebase-guards/auth.guard.js")
13+
14+
mockLoggedOutAuthState()
1215

1316
export const parameters = {
1417
actions: { argTypesRegex: "^on[A-Z].*" },
@@ -60,6 +63,12 @@ export const parameters = {
6063

6164
export const decorators = [
6265
(Story, context) => {
66+
if (typeof window !== "undefined") {
67+
window.__MAPLE_STORYBOOK_ALLOW_FIREBASE__ = Boolean(
68+
context?.parameters?.firebaseGuard?.allow
69+
)
70+
}
71+
6372
return (
6473
<Suspense fallback="Loading...">
6574
<I18nextProvider i18n={i18n}>

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,23 @@ git pull upstream main
6868
- `yarn dev:down`: Stop the application.
6969
- `yarn dev:update`: Update the application images. Run this whenever dependencies in `package.json` change.
7070

71+
### Storybook Firebase Guard
72+
73+
Storybook blocks real Firebase Auth and Firestore calls by default to prevent accidental network access from stories.
74+
75+
If a story triggers a blocked call, Storybook throws an error with guidance about what to mock.
76+
77+
Use these patterns when building stories:
78+
79+
- Prefer passing mock props/data to components instead of rendering hook-driven containers.
80+
- If a component supports injection (for example, mock `profile`/`index` props), use that in the story args.
81+
- For auth-driven stories, use the helpers in [stories/utils/storybookFirebaseAuth.ts](stories/utils/storybookFirebaseAuth.ts) to switch between logged-out and logged-in user mocks.
82+
83+
Opt-out options:
84+
85+
- Per story: set `parameters.firebaseGuard.allow = true`.
86+
- Globally: run Storybook with `STORYBOOK_ALLOW_FIREBASE_CALLS=1`.
87+
7188
Install the [Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) and [React DevTools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) browser extensions if you're developing frontend
7289

7390
## Contributing Backend Features to Dev/Prod:

0 commit comments

Comments
 (0)