From d474331e274fb8a28a39821563f43684b30711cd Mon Sep 17 00:00:00 2001 From: Or Forshmit <162809292+OrF8@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:03:37 +0300 Subject: [PATCH 1/4] fix: resolve eslint hook state-in-effect violations --- CHANGELOG.md | 46 ++++++ README.md | 245 ++++++++++++++++++++------------ SECURITY.md | 65 ++++----- functions/package-lock.json | 2 + functions/package.json | 2 +- package-lock.json | 22 +-- package.json | 2 +- src/hooks/useBoards.js | 73 ++++++---- src/hooks/useIncomingInvites.js | 53 ++++--- src/pages/BoardPage.jsx | 2 +- 10 files changed, 312 insertions(+), 200 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4f90c39 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,46 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/). + +## [Unreleased] + +### Added +- _No entries yet._ + +## [1.1.0] + +### Added +- Excel export for boards using `.xlsx` output. +- Super-board export support with multiple worksheets (one worksheet per sub-board). +- Optional summary worksheet in super-board exports. +- Safer Excel export sanitization for sheet names, file names, and formula-like text values. +- Explicit direct vs. inherited collaborator display in board collaboration management. + +### Changed +- Documentation refresh across README, security policy, and release notes for the `v1.1.0` release. +- Version alignment across frontend and Cloud Functions packages to `1.1.0`. + +### Fixed +- Export date handling and workbook formatting improvements in generated Excel files. +- Export-related CSP allowance updates for ExcelJS-hosted assets. +- Board/super-board export UI flow improvements and related stability fixes. + +### Security +- Security policy updated to supported-version policy for `1.1.x` and private reporting guidance. + +## [1.0.4] + +### Changed +- Dependency and maintenance updates before `1.1.0`. + +## [1.0.1] + +### Changed +- Early post-`1.0.0` maintenance updates. + +## [1.0.0] + +### Added +- Initial public release of the Expense Management application. diff --git a/README.md b/README.md index 6ed0e90..4e079de 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,112 @@ # Expense Management -Expense Management is a real-time collaborative web app for managing shared expenses in Hebrew (RTL). -It is designed for families, roommates, and partners who need one reliable place to track spending, collaborators, -and board hierarchies without exposing broad user data. +Expense Management is a Firebase + React web app for collaborative expense tracking, with a Hebrew RTL UI. +It supports shared boards, one-level board hierarchies ("super boards" with sub-boards), invites, and Excel export. -The project combines a React frontend with Firebase Authentication, Firestore, Hosting, and callable Cloud Functions. -Invite, collaborator, and account management flows are implemented server-side to preserve a least-privilege security model. - -🌐 **[Try the live app β†’](https://of8-expense-management.web.app/)** +🌐 **Live app:** https://of8-expense-management.web.app/

License: MIT - top-language -

-

- react - vite - firebase - tailwind + Release v1.1.0

-## Table of Contents - -- [Overview](#overview) -- [Key Features](#key-features) -- [Tech Stack & Architecture](#tech-stack--architecture) -- [Project Structure](#project-structure) -- [Getting Started](#getting-started) - - [Prerequisites](#prerequisites) - - [Installation](#installation) - - [Environment Configuration](#environment-configuration) - - [Run Locally](#run-locally) -- [Deployment](#deployment) -- [Security Notes](#security-notes) -- [License](#license) - ## Overview -The app centers around collaborative boards. Each board contains transactions, collaborators, and optional sub-boards. -Members see real-time updates, while ownership and membership operations are enforced by Firestore rules and Cloud Functions. +The app is designed for small groups (families, roommates, partners) who manage shared spending. +Each board has members, transactions, and optional hierarchy relationships. +Data is stored in Firestore and updates in real time. + +## Features + +- **Authentication:** Email/password and Google sign-in. +- **Boards:** Create, rename, delete, and view shared boards. +- **Super boards:** Group regular boards under one parent board (one-level hierarchy). +- **Invitations:** Board owners can invite by email; invitees can accept or decline. +- **Membership model:** + - `directMemberUids`: explicitly invited to a board. + - `memberUids`: effective access (direct + inherited from parent board). +- **Inherited access:** Membership flows **down** from a super board to its sub-boards. +- **Transactions:** Create, edit, and delete transactions on regular boards. +- **Amounts:** Positive and negative amounts are supported (useful for refunds/credits). +- **Future dates:** Optional `transactionDate` accepts valid `YYYY-MM-DD` dates, including future dates. +- **Excel export:** + - Regular board: single worksheet export. + - Super board: multi-sheet export (one sheet per sub-board) + optional summary sheet. +- **Account management:** Update nickname, sign out, and delete account (with server-side cleanup). + +## Boards vs. Super Boards + +- **Regular board:** A board without `subBoardIds`; it contains transactions directly. +- **Super board:** A board with one or more `subBoardIds`; it aggregates totals from sub-boards and does not show a transaction-entry view. +- **Sub-board:** A board with `parentBoardId` set. + +Hierarchy is intentionally **one level**: +- A board can be top-level, or a child of one parent board. +- A sub-board cannot itself have sub-boards. + +## Access Model (Direct vs. Inherited) + +This project uses two membership fields: -## Key Features +- `directMemberUids` = users directly added to that board. +- `memberUids` = users with effective access to that board. -- Firebase Auth (email/password + Google) -- Shared expense boards with real-time transaction updates -- Board hierarchy (parent/sub-board relationships) -- Installment-aware credit-card tracking -- Email-based invite flow (create/accept/decline/revoke) -- Owner/member management (remove member, leave board) -- Account deletion with server-side data cleanup -- Hebrew RTL interface with light/dark theme +Behavior: +- Direct membership on a super board grants inherited access to descendant sub-boards. +- Direct membership on a sub-board does **not** grant access to its parent. +- Removing a direct member cascades membership recalculation through descendants. -## Tech Stack & Architecture +## Invitation Flow -- **Frontend:** React 19, Vite 8, React Router 7, Tailwind CSS 4 -- **Backend:** Firebase Cloud Functions (Node.js 22) -- **Data/Auth:** Firestore + Firebase Authentication -- **Hosting:** Firebase Hosting -- **Security:** Firestore rules + App Check enforcement on callable functions +1. Board owner sends an invite by email. +2. Invite document is created under `boards/{boardId}/invites`. +3. Invitee sees incoming invites and can accept/decline. +4. Accepting adds membership and deletes the invite. +5. Declining deletes the invite. + +Notes: +- Invites include `expiresAt` and are treated as pending while the document exists. +- Functions enforce ownership/auth checks before invite and membership mutations. + +## Architecture + +- **Frontend:** React 19 + Vite 8 + React Router 7 + Tailwind CSS 4. +- **Backend:** Firebase Cloud Functions (Node.js 22 runtime). +- **Data/Auth:** Firestore + Firebase Authentication. +- **Hosting:** Firebase Hosting. +- **Security controls:** Firestore Security Rules, App Check integration in the client, and callable functions with `enforceAppCheck: true`. ## Project Structure ```text ExpenseManagement/ β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ components/ # UI and board/collaborator components -β”‚ β”œβ”€β”€ context/ # Auth and theme providers -β”‚ β”œβ”€β”€ firebase/ # Firebase client modules (auth, boards, invites, users, config) -β”‚ β”œβ”€β”€ hooks/ # Data hooks (boards, transactions, incoming invites) -β”‚ └── pages/ # Route pages (auth, boards, board view, legal pages) -β”œβ”€β”€ functions/ # Callable Cloud Functions for invite/member/account flows -β”œβ”€β”€ firestore.rules # Firestore authorization and validation rules -β”œβ”€β”€ firebase.json # Hosting targets, headers, and Firebase service config -└── .github/workflows/ # Deploy + CodeQL workflows +β”‚ β”œβ”€β”€ components/ +β”‚ β”œβ”€β”€ context/ +β”‚ β”œβ”€β”€ firebase/ +β”‚ β”œβ”€β”€ hooks/ +β”‚ β”œβ”€β”€ pages/ +β”‚ └── utils/ +β”œβ”€β”€ functions/ +β”œβ”€β”€ firestore.rules +β”œβ”€β”€ firebase.json +└── .github/workflows/ ``` -## Getting Started +## Setup ### Prerequisites -- Node.js 22+ +- Node.js 22+ (recommended for local dev, including functions) - npm -- Firebase project with Authentication, Firestore, Hosting, and Functions enabled +- Firebase project with: + - Authentication + - Cloud Firestore + - Cloud Functions + - Hosting + - App Check (recommended/enabled for production) -### Installation +### Install ```bash git clone https://github.com/OrF8/ExpenseManagement.git @@ -92,15 +115,15 @@ npm ci npm --prefix functions ci ``` -### Environment Configuration +### Environment Variables -Create `.env` from `.env.example` and fill your Firebase values. +Copy `.env.example` to `.env`: ```bash cp .env.example .env ``` -Required frontend variables: +Required variables: - `VITE_FIREBASE_API_KEY` - `VITE_FIREBASE_AUTH_DOMAIN` @@ -110,64 +133,104 @@ Required frontend variables: - `VITE_FIREBASE_APP_ID` - `VITE_RECAPTCHA_V3_SITE_KEY` -#### Optional: Preview deployment configuration (`.env.preview`) -For preview deployments using the PowerShell script (`npm run deploy:preview -- -PrNumber pr_num`), -create a `.env.preview` file with the same Firebase variables as `.env`, -with two optional variables (if you want to enable App Check debug mode for the preview channel): -- `VITE_APPCHECK_DEBUG=true` -- `VITE_APPCHECK_DEBUG_TOKEN=your_app_check_debug_token_here` - -Another variable is `FIREBASE_PROJECT_ID`. This should be set to the same -project as `.env` if you want to deploy to a preview channel in the same project, -or it can be set to a different project ID if you want to deploy the preview channel to a separate Firebase project. - -This allows you to deploy a Firebase Hosting preview channel separately from your main environment. +Optional preview deployment file: ```bash cp .env.preview.example .env.preview ``` -This is useful for testing changes without affecting the live app. +Additional preview variables: -### Run Locally +- `VITE_APPCHECK_DEBUG` (optional) +- `VITE_APPCHECK_DEBUG_TOKEN` (optional) +- `FIREBASE_PROJECT_ID` (required for preview deploy script) + +## Development + +### Run locally ```bash npm run dev ``` -App URL: `http://localhost:5173` +Vite dev server: `http://localhost:5173` -Production build preview: +### Production build preview ```bash npm run build npm run preview ``` -App URL: `http://localhost:4173` +Preview server: `http://localhost:4173` + +### Linting + +- Frontend linting is configured via ESLint: + +```bash +npm run lint +``` + +- Functions package currently has a placeholder lint script (`Skipping lint`). ## Deployment -Main deployment is automated through GitHub Actions (`.github/workflows/deploy.yml`) using -**Google Workload Identity Federation** (OIDC) with a deploy service account. +### CI/CD (main branch) -Typical manual deploy commands (**after** building with `npm run build`): +GitHub Actions workflow `.github/workflows/deploy.yml`: +- builds the frontend, +- deploys **Functions + Hosting** to Firebase, +- authenticates with Google via Workload Identity Federation (OIDC). + +### Manual deploy ```bash -firebase deploy --only firestore:rules --project +npm run build firebase deploy --only functions,hosting --project ``` -The repo also includes a PowerShell preview script (`npm run deploy:preview -- -PrNumber pr_num`) that deploys functions and a hosting preview channel using `.env.preview`. +To deploy Firestore rules explicitly: + +```bash +firebase deploy --only firestore:rules --project +``` + +### Preview deployment script + +A PowerShell script is provided for preview channels: + +```bash +npm run deploy:preview -- -PrNumber +``` + +This script: +- loads `.env.preview`, +- deploys functions, +- builds with `--mode preview`, +- deploys a Firebase Hosting preview channel. + +## Security & Privacy Notes + +- Firestore rules restrict board reads/writes to authorized users and owners by role. +- Invite and membership mutations are handled through callable functions instead of broad client-side user reads. +- Callable functions are configured with App Check enforcement. +- Account deletion is handled server-side to remove owned data and membership links. +- This is a collaborative app: data shared to a board is visible to that board’s members. + +For vulnerability reporting, see [SECURITY.md](./SECURITY.md). + +## Release Status + +Current release target: **v1.1.0**. -## Security Notes +Notable release focus: +- hierarchy-aware collaboration, +- Excel export improvements (including super-board multi-sheet export), +- documentation refresh and version alignment. -- Firestore access is scoped to board membership and document ownership; `/users/{uid}` is owner-readable and owner-writable only. -- Invite creation, acceptance, declination, revocation, member removal, and account deletion are all implemented as callable Cloud Functions to ensure server-side validation and least-privilege access. -- Callable functions enforce App Check (`enforceAppCheck: true`), and Hosting serves strict security headers (including CSP, X-Frame-Options, and Referrer-Policy). -- App Check is also enforced on Firestore and authentication operations to prevent abuse from unauthorized clients. -- Invite queries use collection-group access constrained by authenticated email match in Firestore rules. +See [CHANGELOG.md](./CHANGELOG.md) for release notes. ## License -This project is licensed under the MIT license. For more information, see the [LICENSE](./LICENSE) file. +MIT. See [LICENSE](./LICENSE). diff --git a/SECURITY.md b/SECURITY.md index 5d6857a..a2ddec5 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,53 +2,48 @@ ## Supported Versions -This project is currently in early development. Only the latest version is supported with security updates. +Security fixes are provided for the latest minor line only. -| Version | Supported | -| -------- | ------------------ | -| 1.0.x | :white_check_mark: | -| < 1.0.0 | :x: | +| Version | Supported | +| --- | --- | +| 1.1.x | βœ… | +| 1.0.x | ❌ | +| < 1.0.0 | ❌ | -We strongly recommend always using the latest version of the application. - ---- +If you run a self-hosted deployment, upgrade to the latest `1.1.x` patch release as soon as practical. ## Reporting a Vulnerability -If you discover a security vulnerability, please report it responsibly. +Please report vulnerabilities **privately**. + +### Preferred reporting channel -### πŸ“¬ How to report -- Open a **private security advisory** on GitHub (preferred), or -- Contact the maintainer directly via GitHub, or -- Contact us via email [expensemanagementwebsite@gmail.com](mailto:expensemanagementwebsite@gmail.com) +Use GitHub’s private vulnerability reporting for this repository: -Please include: -- A clear description of the vulnerability -- Steps to reproduce the issue -- Potential impact -- Suggested fixes (if available) +- Open a private report via the repository’s **Security** tab ("Report a vulnerability") when available. +- If private reporting is not enabled in your view, open a GitHub Security Advisory draft for the repository maintainers. -### ⏱️ Response timeline -- Initial response: within **48–72 hours** -- Status update: within **5–7 days** +Do **not** open a public issue for suspected vulnerabilities. -### πŸ”’ Responsible disclosure -Please **do not publicly disclose** the vulnerability until it has been reviewed and addressed. +### What to include -### βœ… What to expect -- If the vulnerability is accepted: - - It will be fixed as soon as possible - - A patched version will be released -- If declined: - - You will receive an explanation +Please include as much detail as possible: ---- +- Affected area (frontend, Firestore rules, Cloud Functions, deployment config, etc.) +- Steps to reproduce +- Expected vs. actual behavior +- Impact assessment (confidentiality / integrity / availability) +- Proof-of-concept details, logs, or screenshots (if safe to share) +- Suggested remediation (optional) -## Additional Notes +### Disclosure expectations -This project uses: -- Firebase (Firestore, Authentication, Hosting) +- Please do not publicly disclose the issue before a fix is available. +- Maintainers will acknowledge reports and triage based on severity and reproducibility. +- Resolution timelines vary by complexity and maintainer availability. +- When a report is confirmed, the fix will be shipped in a supported release line. -While Firebase provides strong security features, proper configuration (e.g., Firestore rules, API restrictions) is critical. Misconfiguration may lead to vulnerabilities. +## Scope Notes -If your report relates to Firebase configuration, please include relevant details. +This project relies on Firebase services (Authentication, Firestore, Functions, Hosting). +Configuration issues can be security-sensitive; include relevant Firebase project and rule/function context in reports. diff --git a/functions/package-lock.json b/functions/package-lock.json index 3e45e73..71ee4ba 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -1,10 +1,12 @@ { "name": "expense-management-functions", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "expense-management-functions", + "version": "1.1.0", "dependencies": { "firebase-admin": "^13.7.0", "firebase-functions": "^7.2.3" diff --git a/functions/package.json b/functions/package.json index 42ae3ad..b5ea2a3 100644 --- a/functions/package.json +++ b/functions/package.json @@ -22,5 +22,5 @@ "eslint-config-google": "^0.14.0" }, "private": true, - "version": "1.0.4" + "version": "1.1.0" } diff --git a/package-lock.json b/package-lock.json index da458b2..2a633a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "expense-management", - "version": "1.0.4", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "expense-management", - "version": "1.0.4", + "version": "1.1.0", "dependencies": { "firebase": "^12.11.0", "react": "^19.2.4", @@ -1451,9 +1451,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1471,9 +1468,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1491,9 +1485,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1511,9 +1502,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1531,9 +1519,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1551,9 +1536,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/package.json b/package.json index 40bdfdc..954568b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "expense-management", "private": true, - "version": "1.0.4", + "version": "1.1.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src/hooks/useBoards.js b/src/hooks/useBoards.js index 24da487..1a3a37f 100644 --- a/src/hooks/useBoards.js +++ b/src/hooks/useBoards.js @@ -5,49 +5,64 @@ import { subscribeWithAppCheckRetry } from '../utils/appCheckRetry'; export function useBoards() { const { user } = useAuth(); - const [boards, setBoards] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [retryingSecureConnection, setRetryingSecureConnection] = useState(false); + const uid = user?.uid ?? null; - useEffect(() => { - if (!user?.uid) { - setBoards([]); - setLoading(false); - setError(null); - setRetryingSecureConnection(false); - return; - } + const [state, setState] = useState({ + boards: [], + error: null, + retryingSecureConnection: false, + forUid: null, + }); - setLoading(true); - setError(null); - setRetryingSecureConnection(false); + useEffect(() => { + if (!uid) return; const unsubscribe = subscribeWithAppCheckRetry( - (onData, onError) => subscribeToBoards(user.uid, onData, onError), + (onData, onError) => subscribeToBoards(uid, onData, onError), (data) => { - setBoards(data); - setError(null); - setRetryingSecureConnection(false); - setLoading(false); + setState({ + boards: data, + error: null, + retryingSecureConnection: false, + forUid: uid, + }); }, (err) => { - setBoards([]); - setError(err?.message || 'שגיאה Χ‘Χ˜Χ’Χ™Χ Χͺ Χ”ΧœΧ•Χ—Χ•Χͺ'); - setRetryingSecureConnection(false); - setLoading(false); + setState({ + boards: [], + error: err?.message || 'שגיאה Χ‘Χ˜Χ’Χ™Χ Χͺ Χ”ΧœΧ•Χ—Χ•Χͺ', + retryingSecureConnection: false, + forUid: uid, + }); }, { onRetryAttempt: () => { - setRetryingSecureConnection(true); - setLoading(true); - setError(null); + setState((prev) => ({ + ...prev, + retryingSecureConnection: true, + error: null, + })); }, }, ); return () => unsubscribe(); - }, [user?.uid]); + }, [uid]); + + if (!uid) { + return { + boards: [], + loading: false, + error: null, + retryingSecureConnection: false, + }; + } - return { boards, loading, error, retryingSecureConnection }; + const loading = state.forUid !== uid; + return { + boards: loading ? [] : state.boards, + loading, + error: state.error, + retryingSecureConnection: state.retryingSecureConnection, + }; } diff --git a/src/hooks/useIncomingInvites.js b/src/hooks/useIncomingInvites.js index dbd4944..9ad6e1e 100644 --- a/src/hooks/useIncomingInvites.js +++ b/src/hooks/useIncomingInvites.js @@ -4,37 +4,46 @@ import { useAuth } from '../context/AuthContext'; export function useIncomingInvites() { const { user } = useAuth(); - const [invites, setInvites] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const email = user?.email ?? null; - useEffect(() => { - if (!user?.email) { - setInvites([]); - setLoading(false); - setError(null); - return; - } + const [state, setState] = useState({ + invites: [], + error: null, + forEmail: null, + }); - setLoading(true); - setError(null); + useEffect(() => { + if (!email) return; const unsubscribe = subscribeToIncomingInvites( - user.email, + email, (data) => { - setInvites(data); - setError(null); - setLoading(false); + setState({ invites: data, error: null, forEmail: email }); }, (err) => { - setInvites([]); - setError(err?.message || 'שגיאה Χ‘Χ˜Χ’Χ™Χ Χͺ Χ”Χ”Χ–ΧžΧ Χ•Χͺ'); - setLoading(false); - } + setState({ + invites: [], + error: err?.message || 'שגיאה Χ‘Χ˜Χ’Χ™Χ Χͺ Χ”Χ”Χ–ΧžΧ Χ•Χͺ', + forEmail: email, + }); + }, ); return () => unsubscribe(); - }, [user?.email]); + }, [email]); + + if (!email) { + return { + invites: [], + loading: false, + error: null, + }; + } - return { invites, loading, error }; + const loading = state.forEmail !== email; + return { + invites: loading ? [] : state.invites, + loading, + error: state.error, + }; } diff --git a/src/pages/BoardPage.jsx b/src/pages/BoardPage.jsx index 2121912..88ee808 100644 --- a/src/pages/BoardPage.jsx +++ b/src/pages/BoardPage.jsx @@ -271,7 +271,7 @@ export function BoardPage() { b.id !== boardId && isMergeValid(boardId, b.id, allBoards), ); - }, [isOwner, board, allBoards, boardId, user?.uid]); + }, [isOwner, board, isSuperBoard, isSubBoard, allBoards, boardId, user?.uid]); async function handleMoveUnder(parentId) { const parent = allBoards.find((b) => b.id === parentId); From 24cd7566be772178e70ee909ae14dee07631c01d Mon Sep 17 00:00:00 2001 From: or-forshmit8 Date: Wed, 8 Apr 2026 21:15:48 +0300 Subject: [PATCH 2/4] docs: refresh documentation for v1.1.0 release --- CHANGELOG.md | 8 +++++--- README.md | 44 +++++++++++++++++++++++++++++++++----------- SECURITY.md | 8 +++++++- 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f90c39..48ea01f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,12 @@ All notable changes to this project are documented in this file. The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/). -## [Unreleased] +[//]: # (## [Unreleased]) -### Added -- _No entries yet._ +[//]: # () +[//]: # (### Added) + +[//]: # (- _No entries yet._) ## [1.1.0] diff --git a/README.md b/README.md index 4e079de..8fa9c11 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,31 @@ It supports shared boards, one-level board hierarchies ("super boards" with sub-

License: MIT + top-language Release v1.1.0

+

+ react + vite + firebase + tailwind +

+ +## Table of Contents + +- [Overview](#overview) +- [Features](#Features) +- [Boards vs. Super Boards](#boards-vs-super-boards) +- [Access Model (Direct vs. Inherited)](#access-model-direct-vs-inherited) +- [Invitation Flow](#invitation-flow) +- [Architecture](#architecture) +- [Project Structure](#project-structure) +- [Setup](#setup) +- [Development](#development) +- [Deployment](#deployment) +- [Security & Privacy Notes](#security--privacy-notes) +- [Release Status](#release-status) +- [License](#license) ## Overview @@ -81,16 +104,15 @@ Notes: ```text ExpenseManagement/ β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ components/ -β”‚ β”œβ”€β”€ context/ -β”‚ β”œβ”€β”€ firebase/ -β”‚ β”œβ”€β”€ hooks/ -β”‚ β”œβ”€β”€ pages/ -β”‚ └── utils/ -β”œβ”€β”€ functions/ -β”œβ”€β”€ firestore.rules -β”œβ”€β”€ firebase.json -└── .github/workflows/ +β”‚ β”œβ”€β”€ components/ # UI and board/collaborator components +β”‚ β”œβ”€β”€ context/ # Auth and theme providers +β”‚ β”œβ”€β”€ firebase/ # Firebase client modules (auth, boards, invites, users, config) +β”‚ β”œβ”€β”€ hooks/ # Data hooks (boards, transactions, incoming invites) +β”‚ └── pages/ # Route pages (auth, boards, board view, legal pages) +β”œβ”€β”€ functions/ # Callable Cloud Functions for invite/member/account flows +β”œβ”€β”€ firestore.rules # Firestore authorization and validation rules +β”œβ”€β”€ firebase.json # Hosting targets, headers, and Firebase service config +└── .github/workflows/ # Deploy + CodeQL workflows ``` ## Setup @@ -233,4 +255,4 @@ See [CHANGELOG.md](./CHANGELOG.md) for release notes. ## License -MIT. See [LICENSE](./LICENSE). +This project is licensed under the MIT license. For more information, see the [LICENSE](./LICENSE) file. diff --git a/SECURITY.md b/SECURITY.md index a2ddec5..c49ecf5 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -20,11 +20,17 @@ Please report vulnerabilities **privately**. Use GitHub’s private vulnerability reporting for this repository: -- Open a private report via the repository’s **Security** tab ("Report a vulnerability") when available. +- Open a private report via the repository’s **Security** tab ("Report a vulnerability"). - If private reporting is not enabled in your view, open a GitHub Security Advisory draft for the repository maintainers. Do **not** open a public issue for suspected vulnerabilities. +#### Other reporting channels + +- Contact the maintainer directly via GitHub +- Contact us via email [expensemanagementwebsite@gmail.com](mailto:expensemanagementwebsite@gmail.com) + + ### What to include Please include as much detail as possible: From b37602f41d7fe9eae650124b63858a4587522b68 Mon Sep 17 00:00:00 2001 From: Or Forshmit <162809292+OrF8@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:39:35 +0300 Subject: [PATCH 3/4] Fix stale hook state during auth identity switches --- src/hooks/useBoards.js | 15 +++++++++++---- src/hooks/useIncomingInvites.js | 18 ++++++++++++++---- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/hooks/useBoards.js b/src/hooks/useBoards.js index 1a3a37f..a386d78 100644 --- a/src/hooks/useBoards.js +++ b/src/hooks/useBoards.js @@ -6,7 +6,6 @@ import { subscribeWithAppCheckRetry } from '../utils/appCheckRetry'; export function useBoards() { const { user } = useAuth(); const uid = user?.uid ?? null; - const [state, setState] = useState({ boards: [], error: null, @@ -46,7 +45,15 @@ export function useBoards() { }, ); - return () => unsubscribe(); + return () => { + unsubscribe(); + setState({ + boards: [], + error: null, + retryingSecureConnection: false, + forUid: null, + }); + }; }, [uid]); if (!uid) { @@ -62,7 +69,7 @@ export function useBoards() { return { boards: loading ? [] : state.boards, loading, - error: state.error, - retryingSecureConnection: state.retryingSecureConnection, + error: loading ? null : state.error, + retryingSecureConnection: loading ? false : state.retryingSecureConnection, }; } diff --git a/src/hooks/useIncomingInvites.js b/src/hooks/useIncomingInvites.js index 9ad6e1e..64b2609 100644 --- a/src/hooks/useIncomingInvites.js +++ b/src/hooks/useIncomingInvites.js @@ -5,7 +5,6 @@ import { useAuth } from '../context/AuthContext'; export function useIncomingInvites() { const { user } = useAuth(); const email = user?.email ?? null; - const [state, setState] = useState({ invites: [], error: null, @@ -18,7 +17,11 @@ export function useIncomingInvites() { const unsubscribe = subscribeToIncomingInvites( email, (data) => { - setState({ invites: data, error: null, forEmail: email }); + setState({ + invites: data, + error: null, + forEmail: email, + }); }, (err) => { setState({ @@ -29,7 +32,14 @@ export function useIncomingInvites() { }, ); - return () => unsubscribe(); + return () => { + unsubscribe(); + setState({ + invites: [], + error: null, + forEmail: null, + }); + }; }, [email]); if (!email) { @@ -44,6 +54,6 @@ export function useIncomingInvites() { return { invites: loading ? [] : state.invites, loading, - error: state.error, + error: loading ? null : state.error, }; } From 8bc0471ec23fb7060605cfa7fc8fd097673e0569 Mon Sep 17 00:00:00 2001 From: Or Forshmit <162809292+OrF8@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:47:48 +0300 Subject: [PATCH 4/4] Address review follow-ups for board and invite hooks --- src/hooks/useBoards.js | 24 ++++++++++++------------ src/hooks/useIncomingInvites.js | 16 ++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/hooks/useBoards.js b/src/hooks/useBoards.js index a386d78..e700e2a 100644 --- a/src/hooks/useBoards.js +++ b/src/hooks/useBoards.js @@ -11,6 +11,8 @@ export function useBoards() { error: null, retryingSecureConnection: false, forUid: null, + forUser: null, + retryingForUser: null, }); useEffect(() => { @@ -24,6 +26,8 @@ export function useBoards() { error: null, retryingSecureConnection: false, forUid: uid, + forUser: user, + retryingForUser: null, }); }, (err) => { @@ -32,6 +36,8 @@ export function useBoards() { error: err?.message || 'שגיאה Χ‘Χ˜Χ’Χ™Χ Χͺ Χ”ΧœΧ•Χ—Χ•Χͺ', retryingSecureConnection: false, forUid: uid, + forUser: user, + retryingForUser: null, }); }, { @@ -40,21 +46,14 @@ export function useBoards() { ...prev, retryingSecureConnection: true, error: null, + retryingForUser: user, })); }, }, ); - return () => { - unsubscribe(); - setState({ - boards: [], - error: null, - retryingSecureConnection: false, - forUid: null, - }); - }; - }, [uid]); + return () => unsubscribe(); + }, [uid, user]); if (!uid) { return { @@ -65,11 +64,12 @@ export function useBoards() { }; } - const loading = state.forUid !== uid; + const loading = state.forUid !== uid || state.forUser !== user; return { boards: loading ? [] : state.boards, loading, error: loading ? null : state.error, - retryingSecureConnection: loading ? false : state.retryingSecureConnection, + retryingSecureConnection: + state.retryingSecureConnection && state.retryingForUser === user, }; } diff --git a/src/hooks/useIncomingInvites.js b/src/hooks/useIncomingInvites.js index 64b2609..5e9c5ce 100644 --- a/src/hooks/useIncomingInvites.js +++ b/src/hooks/useIncomingInvites.js @@ -9,6 +9,7 @@ export function useIncomingInvites() { invites: [], error: null, forEmail: null, + forUser: null, }); useEffect(() => { @@ -21,6 +22,7 @@ export function useIncomingInvites() { invites: data, error: null, forEmail: email, + forUser: user, }); }, (err) => { @@ -28,19 +30,13 @@ export function useIncomingInvites() { invites: [], error: err?.message || 'שגיאה Χ‘Χ˜Χ’Χ™Χ Χͺ Χ”Χ”Χ–ΧžΧ Χ•Χͺ', forEmail: email, + forUser: user, }); }, ); - return () => { - unsubscribe(); - setState({ - invites: [], - error: null, - forEmail: null, - }); - }; - }, [email]); + return () => unsubscribe(); + }, [email, user]); if (!email) { return { @@ -50,7 +46,7 @@ export function useIncomingInvites() { }; } - const loading = state.forEmail !== email; + const loading = state.forEmail !== email || state.forUser !== user; return { invites: loading ? [] : state.invites, loading,