chore: remove Firebase backend code, replaced by Supabase (see 2026-08-08 spec)
Deleted all Firebase-specific files and directories: - Configuration: .firebaserc, firebase.json, firestore.* files, storage.rules - Cloud Functions: functions/ directory (package.json, source, config) - Tests: firebase-tests/, vitest.rules.config.ts - Scripts: scripts/seed.ts - Source: src/lib/firebase.ts and .test.ts - Environment: .env.example Removed dependencies: - firebase (main package) - firebase-admin, firebase-tools (dev) - @firebase/rules-unit-testing (dev) Removed scripts: - test:rules (Firebase emulator tests) - seed (Firestore seed script) Updated package.json via npm install (729 packages removed) Updated .gitignore (removed functions/ entries) Updated vite.config.ts (removed firebase-tests and functions exclusions) All tests, builds, and lints pass cleanly. No Firebase references remain in the codebase (only in historical spec documents in docs/). Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
VITE_FIREBASE_API_KEY=
|
||||
VITE_FIREBASE_AUTH_DOMAIN=
|
||||
VITE_FIREBASE_PROJECT_ID=
|
||||
VITE_FIREBASE_STORAGE_BUCKET=
|
||||
VITE_FIREBASE_MESSAGING_SENDER_ID=
|
||||
VITE_FIREBASE_APP_ID=
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"projects": {
|
||||
"default": "demo-braids-by-mira"
|
||||
}
|
||||
}
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -12,10 +12,6 @@ dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Cloud Functions
|
||||
functions/lib
|
||||
functions/node_modules
|
||||
|
||||
# Environment variables
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Phase 2A — Remaining work before this backend goes live
|
||||
|
||||
This backend (Firestore, Storage, Cloud Functions) was built entirely with placeholder
|
||||
values because Task 1 of `docs/superpowers/plans/2026-08-04-phase2a-backend-foundation.md`
|
||||
(manual Firebase/Resend account setup) hadn't been done yet. Before deploying for real:
|
||||
|
||||
## 1. Complete Task 1 of the plan
|
||||
Create the Firebase project, upgrade to Blaze, enable Firestore/Auth/Storage,
|
||||
create Mira's admin user, register a web app, create a Resend account + API key,
|
||||
authenticate the Firebase CLI. Full instructions are in the plan's Task 1.
|
||||
|
||||
## 2. Replace these 4 placeholder values with real ones from Task 1
|
||||
|
||||
| Placeholder | Real value goes in | Also appears in |
|
||||
|---|---|---|
|
||||
| `demo-braids-by-mira` (fake project id) | `.firebaserc` | `package.json`'s `test:rules` script, `firebase-tests/firestore.rules.test.ts` |
|
||||
| `dev-admin-placeholder-uid` (fake admin UID) | `firestore.rules`, `storage.rules` | `firebase-tests/firestore.rules.test.ts` |
|
||||
| `bookings@braidsbymira.com` (placeholder from-address) | `functions/src/index.ts` (`FROM_ADDRESS`) | — must match a Resend-verified sending domain |
|
||||
| `hello@braidsbymira.com` (placeholder Mira email) | `functions/src/index.ts` (`MIRA_EMAIL`) | — replace only if Mira's real notification inbox differs |
|
||||
|
||||
Also update `.env.local` (gitignored, not committed) with the real Firebase web app
|
||||
config from Task 1, Step 5 — copy `.env.example` if you haven't already.
|
||||
|
||||
## 3. Run the one manual CLI step Task 6 skipped
|
||||
|
||||
```bash
|
||||
npx firebase-tools functions:secrets:set RESEND_API_KEY
|
||||
```
|
||||
|
||||
## 4. Run Task 8 of the plan
|
||||
|
||||
Deploy rules + functions, seed production data, and run the two live smoke tests
|
||||
(create a real booking/inquiry document, confirm `bookedSlots` updates and email
|
||||
arrives). Full steps are in the plan's Task 8.
|
||||
|
||||
## 5. Delete this file
|
||||
|
||||
Once all of the above is done and verified, delete this file — it's a temporary
|
||||
tracking note, not permanent documentation.
|
||||
@@ -1,154 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, it, beforeAll, afterAll, beforeEach } from 'vitest'
|
||||
import {
|
||||
initializeTestEnvironment,
|
||||
assertSucceeds,
|
||||
assertFails,
|
||||
type RulesTestEnvironment,
|
||||
} from '@firebase/rules-unit-testing'
|
||||
import { doc, getDoc, setDoc, addDoc, collection } from 'firebase/firestore'
|
||||
|
||||
const ADMIN_UID = 'dev-admin-placeholder-uid'
|
||||
|
||||
let testEnv: RulesTestEnvironment
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await initializeTestEnvironment({
|
||||
projectId: 'demo-braids-by-mira',
|
||||
firestore: {
|
||||
rules: readFileSync('firestore.rules', 'utf8'),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await testEnv.cleanup()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await testEnv.clearFirestore()
|
||||
})
|
||||
|
||||
describe('Firestore security rules', () => {
|
||||
it('allows anyone to read products', async () => {
|
||||
await testEnv.withSecurityRulesDisabled(async (ctx) => {
|
||||
await setDoc(doc(ctx.firestore(), 'products/edge-control'), { name: 'Edge Control' })
|
||||
})
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertSucceeds(getDoc(doc(unauthed.firestore(), 'products/edge-control')))
|
||||
})
|
||||
|
||||
it('blocks a non-admin from writing to products', async () => {
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertFails(setDoc(doc(unauthed.firestore(), 'products/hacked'), { name: 'Hack' }))
|
||||
})
|
||||
|
||||
it('allows the admin to write to products', async () => {
|
||||
const admin = testEnv.authenticatedContext(ADMIN_UID)
|
||||
await assertSucceeds(setDoc(doc(admin.firestore(), 'products/new-item'), { name: 'New Item' }))
|
||||
})
|
||||
|
||||
it('allows anyone to create a booking', async () => {
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertSucceeds(
|
||||
addDoc(collection(unauthed.firestore(), 'bookings'), {
|
||||
serviceId: 'knotless',
|
||||
date: '2026-09-01',
|
||||
time: '10:00 AM',
|
||||
contact: { name: 'Jane', phone: '555-0100', email: 'jane@example.com', notes: '' },
|
||||
status: 'confirmed',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('blocks creating a booking that is missing a required field', async () => {
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertFails(
|
||||
addDoc(collection(unauthed.firestore(), 'bookings'), {
|
||||
serviceId: 'knotless',
|
||||
date: '2026-09-01',
|
||||
time: '10:00 AM',
|
||||
status: 'confirmed',
|
||||
// missing contact
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('blocks creating a booking with a malformed date', async () => {
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertFails(
|
||||
addDoc(collection(unauthed.firestore(), 'bookings'), {
|
||||
serviceId: 'knotless',
|
||||
date: 'not-a-date',
|
||||
time: '10:00 AM',
|
||||
contact: { name: 'Jane', phone: '555-0100', email: 'jane@example.com', notes: '' },
|
||||
status: 'confirmed',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('blocks a non-admin from reading bookings (PII protection)', async () => {
|
||||
await testEnv.withSecurityRulesDisabled(async (ctx) => {
|
||||
await setDoc(doc(ctx.firestore(), 'bookings/b1'), {
|
||||
serviceId: 'knotless',
|
||||
contact: { name: 'Jane', phone: '555-0100', email: 'jane@example.com', notes: '' },
|
||||
})
|
||||
})
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertFails(getDoc(doc(unauthed.firestore(), 'bookings/b1')))
|
||||
})
|
||||
|
||||
it('allows the admin to read bookings', async () => {
|
||||
await testEnv.withSecurityRulesDisabled(async (ctx) => {
|
||||
await setDoc(doc(ctx.firestore(), 'bookings/b1'), { serviceId: 'knotless' })
|
||||
})
|
||||
const admin = testEnv.authenticatedContext(ADMIN_UID)
|
||||
await assertSucceeds(getDoc(doc(admin.firestore(), 'bookings/b1')))
|
||||
})
|
||||
|
||||
it('allows anyone to create an inquiry with a valid shape', async () => {
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertSucceeds(
|
||||
addDoc(collection(unauthed.firestore(), 'inquiries'), {
|
||||
productId: 'edge-control',
|
||||
contact: { name: 'Jane', email: 'jane@example.com', notes: '' },
|
||||
status: 'new',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('blocks creating an inquiry that is missing a required field', async () => {
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertFails(
|
||||
addDoc(collection(unauthed.firestore(), 'inquiries'), {
|
||||
productId: 'edge-control',
|
||||
status: 'new',
|
||||
// missing contact
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('blocks a non-admin from reading inquiries (PII protection)', async () => {
|
||||
await testEnv.withSecurityRulesDisabled(async (ctx) => {
|
||||
await setDoc(doc(ctx.firestore(), 'inquiries/i1'), {
|
||||
productId: 'edge-control',
|
||||
contact: { name: 'Jane', email: 'jane@example.com', notes: '' },
|
||||
})
|
||||
})
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertFails(getDoc(doc(unauthed.firestore(), 'inquiries/i1')))
|
||||
})
|
||||
|
||||
it('allows anyone to read bookedSlots (no PII there)', async () => {
|
||||
await testEnv.withSecurityRulesDisabled(async (ctx) => {
|
||||
await setDoc(doc(ctx.firestore(), 'bookedSlots/2026-09-01'), { times: ['10:00 AM'] })
|
||||
})
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertSucceeds(getDoc(doc(unauthed.firestore(), 'bookedSlots/2026-09-01')))
|
||||
})
|
||||
|
||||
it('blocks a non-admin from writing bookedSlots directly', async () => {
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertFails(setDoc(doc(unauthed.firestore(), 'bookedSlots/2026-09-01'), { times: ['1:00 PM'] }))
|
||||
})
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"firestore": {
|
||||
"rules": "firestore.rules",
|
||||
"indexes": "firestore.indexes.json"
|
||||
},
|
||||
"storage": {
|
||||
"rules": "storage.rules"
|
||||
},
|
||||
"functions": [
|
||||
{
|
||||
"source": "functions",
|
||||
"codebase": "default",
|
||||
"ignore": ["node_modules", ".git", "firebase-debug.log", "firebase-debug.*.log"]
|
||||
}
|
||||
],
|
||||
"emulators": {
|
||||
"auth": { "port": 9099 },
|
||||
"functions": { "port": 5001 },
|
||||
"firestore": { "port": 8080 },
|
||||
"storage": { "port": 9199 },
|
||||
"ui": { "enabled": true }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"indexes": [],
|
||||
"fieldOverrides": []
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
rules_version = '2';
|
||||
service cloud.firestore {
|
||||
match /databases/{database}/documents {
|
||||
function isAdmin() {
|
||||
return request.auth != null && request.auth.uid == "dev-admin-placeholder-uid";
|
||||
}
|
||||
|
||||
match /products/{productId} {
|
||||
allow read: if true;
|
||||
allow write: if isAdmin();
|
||||
}
|
||||
|
||||
match /services/{serviceId} {
|
||||
allow read: if true;
|
||||
allow write: if isAdmin();
|
||||
}
|
||||
|
||||
match /scheduleTemplate/{docId} {
|
||||
allow read: if true;
|
||||
allow write: if isAdmin();
|
||||
}
|
||||
|
||||
match /scheduleExceptions/{date} {
|
||||
allow read: if true;
|
||||
allow write: if isAdmin();
|
||||
}
|
||||
|
||||
match /bookedSlots/{date} {
|
||||
allow read: if true;
|
||||
allow write: if isAdmin();
|
||||
}
|
||||
|
||||
match /bookings/{bookingId} {
|
||||
allow create: if request.resource.data.keys().hasAll(['serviceId', 'date', 'time', 'contact', 'status'])
|
||||
&& request.resource.data.date is string
|
||||
&& request.resource.data.date.matches('^[0-9]{4}-[0-9]{2}-[0-9]{2}$')
|
||||
&& request.resource.data.time is string
|
||||
&& request.resource.data.contact is map
|
||||
&& request.resource.data.contact.keys().hasAll(['name', 'phone', 'email', 'notes'])
|
||||
&& request.resource.data.status is string;
|
||||
allow read, update, delete: if isAdmin();
|
||||
}
|
||||
|
||||
match /inquiries/{inquiryId} {
|
||||
allow create: if request.resource.data.keys().hasAll(['productId', 'contact', 'status'])
|
||||
&& request.resource.data.productId is string
|
||||
&& request.resource.data.contact is map
|
||||
&& request.resource.data.contact.keys().hasAll(['name', 'email', 'notes'])
|
||||
&& request.resource.data.status is string;
|
||||
allow read, update, delete: if isAdmin();
|
||||
}
|
||||
}
|
||||
}
|
||||
4228
functions/package-lock.json
generated
4228
functions/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "functions",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": { "node": "22" },
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"firebase-admin": "^12.0.0",
|
||||
"firebase-functions": "^5.0.0",
|
||||
"resend": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.0",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { buildBookingNotificationEmail, buildInquiryNotificationEmail } from './email.js'
|
||||
|
||||
describe('buildBookingNotificationEmail', () => {
|
||||
it('includes the service, date, time, and contact details', () => {
|
||||
const { subject, html } = buildBookingNotificationEmail({
|
||||
serviceName: 'Knotless Braids',
|
||||
date: '2026-09-01',
|
||||
time: '10:00 AM',
|
||||
contact: { name: 'Jane Doe', phone: '555-0100', email: 'jane@example.com', notes: 'First time client' },
|
||||
})
|
||||
expect(subject).toBe('New booking: Knotless Braids on 2026-09-01')
|
||||
expect(html).toContain('Jane Doe')
|
||||
expect(html).toContain('Knotless Braids')
|
||||
expect(html).toContain('2026-09-01')
|
||||
expect(html).toContain('10:00 AM')
|
||||
expect(html).toContain('555-0100')
|
||||
expect(html).toContain('jane@example.com')
|
||||
expect(html).toContain('First time client')
|
||||
})
|
||||
|
||||
it('omits the notes paragraph when notes are empty', () => {
|
||||
const { html } = buildBookingNotificationEmail({
|
||||
serviceName: 'Cornrows',
|
||||
date: '2026-09-02',
|
||||
time: '1:00 PM',
|
||||
contact: { name: 'Amy', phone: '555-0101', email: 'amy@example.com', notes: '' },
|
||||
})
|
||||
expect(html).not.toContain('Notes:')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildInquiryNotificationEmail', () => {
|
||||
it('includes the product and contact details', () => {
|
||||
const { subject, html } = buildInquiryNotificationEmail({
|
||||
productName: 'Silk Edge Control',
|
||||
contact: { name: 'Nadia', email: 'nadia@example.com', notes: 'Is this in stock?' },
|
||||
})
|
||||
expect(subject).toBe('New inquiry: Silk Edge Control')
|
||||
expect(html).toContain('Nadia')
|
||||
expect(html).toContain('nadia@example.com')
|
||||
expect(html).toContain('Silk Edge Control')
|
||||
expect(html).toContain('Is this in stock?')
|
||||
})
|
||||
|
||||
it('omits the notes paragraph when notes are empty', () => {
|
||||
const { html } = buildInquiryNotificationEmail({
|
||||
productName: 'Braid Oil',
|
||||
contact: { name: 'Kim', email: 'kim@example.com', notes: '' },
|
||||
})
|
||||
expect(html).not.toContain('Notes:')
|
||||
})
|
||||
})
|
||||
@@ -1,28 +0,0 @@
|
||||
export interface BookingEmailInput {
|
||||
serviceName: string
|
||||
date: string
|
||||
time: string
|
||||
contact: { name: string; phone: string; email: string; notes: string }
|
||||
}
|
||||
|
||||
export function buildBookingNotificationEmail(input: BookingEmailInput): { subject: string; html: string } {
|
||||
const notesLine = input.contact.notes ? `<p>Notes: ${input.contact.notes}</p>` : ''
|
||||
return {
|
||||
subject: `New booking: ${input.serviceName} on ${input.date}`,
|
||||
html: `<p><strong>${input.contact.name}</strong> booked <strong>${input.serviceName}</strong> on ${input.date} at ${input.time}.</p>` +
|
||||
`<p>Phone: ${input.contact.phone}<br/>Email: ${input.contact.email}</p>${notesLine}`,
|
||||
}
|
||||
}
|
||||
|
||||
export interface InquiryEmailInput {
|
||||
productName: string
|
||||
contact: { name: string; email: string; notes: string }
|
||||
}
|
||||
|
||||
export function buildInquiryNotificationEmail(input: InquiryEmailInput): { subject: string; html: string } {
|
||||
const notesLine = input.contact.notes ? `<p>Notes: ${input.contact.notes}</p>` : ''
|
||||
return {
|
||||
subject: `New inquiry: ${input.productName}`,
|
||||
html: `<p><strong>${input.contact.name}</strong> (${input.contact.email}) asked about <strong>${input.productName}</strong>.</p>${notesLine}`,
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { onDocumentCreated } from 'firebase-functions/v2/firestore'
|
||||
import { defineSecret } from 'firebase-functions/params'
|
||||
import { initializeApp } from 'firebase-admin/app'
|
||||
import { getFirestore, FieldValue } from 'firebase-admin/firestore'
|
||||
import { Resend } from 'resend'
|
||||
import { buildBookingNotificationEmail, buildInquiryNotificationEmail } from './email.js'
|
||||
|
||||
initializeApp()
|
||||
|
||||
const resendApiKey = defineSecret('RESEND_API_KEY')
|
||||
const FROM_ADDRESS = 'bookings@braidsbymira.com'
|
||||
const MIRA_EMAIL = 'hello@braidsbymira.com'
|
||||
|
||||
export const onBookingCreated = onDocumentCreated(
|
||||
{ document: 'bookings/{bookingId}', secrets: [resendApiKey] },
|
||||
async (event) => {
|
||||
const booking = event.data?.data()
|
||||
if (!booking) return
|
||||
if (typeof booking.date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(booking.date)) return
|
||||
if (typeof booking.time !== 'string' || booking.time.length === 0) return
|
||||
|
||||
const db = getFirestore()
|
||||
await db
|
||||
.doc(`bookedSlots/${booking.date}`)
|
||||
.set({ times: FieldValue.arrayUnion(booking.time) }, { merge: true })
|
||||
|
||||
const resend = new Resend(resendApiKey.value())
|
||||
const { subject, html } = buildBookingNotificationEmail({
|
||||
serviceName: booking.serviceId,
|
||||
date: booking.date,
|
||||
time: booking.time,
|
||||
contact: booking.contact,
|
||||
})
|
||||
await resend.emails.send({ from: FROM_ADDRESS, to: MIRA_EMAIL, subject, html })
|
||||
},
|
||||
)
|
||||
|
||||
export const onInquiryCreated = onDocumentCreated(
|
||||
{ document: 'inquiries/{inquiryId}', secrets: [resendApiKey] },
|
||||
async (event) => {
|
||||
const inquiry = event.data?.data()
|
||||
if (!inquiry) return
|
||||
|
||||
const resend = new Resend(resendApiKey.value())
|
||||
const { subject, html } = buildInquiryNotificationEmail({
|
||||
productName: inquiry.productId,
|
||||
contact: inquiry.contact,
|
||||
})
|
||||
await resend.emails.send({ from: FROM_ADDRESS, to: MIRA_EMAIL, subject, html })
|
||||
},
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"target": "es2022",
|
||||
"outDir": "lib",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
})
|
||||
9970
package-lock.json
generated
9970
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,13 +8,10 @@
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"test:rules": "firebase emulators:exec --only firestore,storage --project demo-braids-by-mira \"vitest run --config vitest.rules.config.ts\"",
|
||||
"seed": "tsx scripts/seed.ts"
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/fiber": "^9.7.0",
|
||||
"firebase": "^12.17.1",
|
||||
"framer-motion": "^12.43.0",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
@@ -23,7 +20,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@firebase/rules-unit-testing": "^5.0.1",
|
||||
"@testing-library/jest-dom": "^7.0.0",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -35,11 +31,8 @@
|
||||
"eslint": "^10.8.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.3",
|
||||
"firebase-admin": "^14.2.0",
|
||||
"firebase-tools": "^15.25.1",
|
||||
"globals": "^17.7.0",
|
||||
"jsdom": "^30.0.1",
|
||||
"tsx": "^4.23.5",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"vite": "^8.2.0",
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { initializeApp } from 'firebase-admin/app'
|
||||
import { getFirestore } from 'firebase-admin/firestore'
|
||||
import { products } from '../src/data/products.js'
|
||||
import { services } from '../src/data/services.js'
|
||||
|
||||
initializeApp()
|
||||
const db = getFirestore()
|
||||
|
||||
function toStoragePath(image: string): string {
|
||||
return image.replace(/^\//, '')
|
||||
}
|
||||
|
||||
async function seed() {
|
||||
const batch = db.batch()
|
||||
|
||||
for (const product of products) {
|
||||
const { id, image, ...rest } = product
|
||||
batch.set(db.doc(`products/${id}`), { ...rest, image: toStoragePath(image) })
|
||||
}
|
||||
|
||||
for (const service of services) {
|
||||
const { id, image, ...rest } = service
|
||||
batch.set(db.doc(`services/${id}`), { ...rest, image: toStoragePath(image) })
|
||||
}
|
||||
|
||||
batch.set(db.doc('scheduleTemplate/default'), {
|
||||
workingDays: [2, 3, 4, 5, 6],
|
||||
startTime: '09:00',
|
||||
endTime: '18:00',
|
||||
slotMinutes: 120,
|
||||
})
|
||||
|
||||
await batch.commit()
|
||||
console.log(`Seeded ${products.length} products, ${services.length} services, and the default schedule.`)
|
||||
}
|
||||
|
||||
seed().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,42 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
const initializeAppMock = vi.fn(() => ({ name: 'mock-app' }))
|
||||
const getFirestoreMock = vi.fn(() => ({ type: 'mock-firestore' }))
|
||||
const getAuthMock = vi.fn(() => ({ type: 'mock-auth' }))
|
||||
const getStorageMock = vi.fn(() => ({ type: 'mock-storage' }))
|
||||
|
||||
vi.mock('firebase/app', () => ({ initializeApp: initializeAppMock }))
|
||||
vi.mock('firebase/firestore', () => ({ getFirestore: getFirestoreMock }))
|
||||
vi.mock('firebase/auth', () => ({ getAuth: getAuthMock }))
|
||||
vi.mock('firebase/storage', () => ({ getStorage: getStorageMock }))
|
||||
|
||||
describe('firebase client setup', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
initializeAppMock.mockClear()
|
||||
})
|
||||
|
||||
it('initializes the app with config read from Vite env vars and exports db/auth/storage', async () => {
|
||||
vi.stubEnv('VITE_FIREBASE_API_KEY', 'test-api-key')
|
||||
vi.stubEnv('VITE_FIREBASE_AUTH_DOMAIN', 'test.firebaseapp.com')
|
||||
vi.stubEnv('VITE_FIREBASE_PROJECT_ID', 'test-project')
|
||||
vi.stubEnv('VITE_FIREBASE_STORAGE_BUCKET', 'test-project.appspot.com')
|
||||
vi.stubEnv('VITE_FIREBASE_MESSAGING_SENDER_ID', '123')
|
||||
vi.stubEnv('VITE_FIREBASE_APP_ID', 'app-id')
|
||||
|
||||
const { app, db, auth, storage } = await import('./firebase')
|
||||
|
||||
expect(initializeAppMock).toHaveBeenCalledWith({
|
||||
apiKey: 'test-api-key',
|
||||
authDomain: 'test.firebaseapp.com',
|
||||
projectId: 'test-project',
|
||||
storageBucket: 'test-project.appspot.com',
|
||||
messagingSenderId: '123',
|
||||
appId: 'app-id',
|
||||
})
|
||||
expect(app).toEqual({ name: 'mock-app' })
|
||||
expect(db).toEqual({ type: 'mock-firestore' })
|
||||
expect(auth).toEqual({ type: 'mock-auth' })
|
||||
expect(storage).toEqual({ type: 'mock-storage' })
|
||||
})
|
||||
})
|
||||
@@ -1,18 +0,0 @@
|
||||
import { initializeApp } from 'firebase/app'
|
||||
import { getFirestore } from 'firebase/firestore'
|
||||
import { getAuth } from 'firebase/auth'
|
||||
import { getStorage } from 'firebase/storage'
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
|
||||
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
|
||||
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
|
||||
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: import.meta.env.VITE_FIREBASE_APP_ID,
|
||||
}
|
||||
|
||||
export const app = initializeApp(firebaseConfig)
|
||||
export const db = getFirestore(app)
|
||||
export const auth = getAuth(app)
|
||||
export const storage = getStorage(app)
|
||||
@@ -1,23 +0,0 @@
|
||||
rules_version = '2';
|
||||
service firebase.storage {
|
||||
match /b/{bucket}/o {
|
||||
function isAdmin() {
|
||||
return request.auth != null && request.auth.uid == "dev-admin-placeholder-uid";
|
||||
}
|
||||
|
||||
match /products/{fileName} {
|
||||
allow read: if true;
|
||||
allow write: if isAdmin();
|
||||
}
|
||||
|
||||
match /styles/{fileName} {
|
||||
allow read: if true;
|
||||
allow write: if isAdmin();
|
||||
}
|
||||
|
||||
match /about/{fileName} {
|
||||
allow read: if true;
|
||||
allow write: if isAdmin();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,6 @@ export default defineConfig({
|
||||
'**/cypress/**',
|
||||
'**/.{idea,git,cache,output,temp}/**',
|
||||
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',
|
||||
'firebase-tests/**',
|
||||
'functions/**',
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['firebase-tests/**/*.test.ts'],
|
||||
testTimeout: 20000,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user