Skip to content

Commit 656af08

Browse files
authored
Merge pull request #197 from feat/sass-auth-gating
feat: implement SaaS auth gating with tiered access model
2 parents 866bead + af73256 commit 656af08

43 files changed

Lines changed: 2360 additions & 263 deletions

Some content is hidden

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

AGENTS.md

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,33 @@
1010
- Repo: `https://github.com/ayoub3bidi/bayan-flow` · prod `main` → bayanflow.com · dev `develop` → dev.bayanflow.com
1111
- License: `Elastic-2.0 OR Commercial` (dual-license)
1212
- Hosting: **Cloudflare Workers** (static SPA via `wrangler.jsonc`); `netlify.toml` kept for rollback only — CI deploys through `.github/workflows/deploy-cloudflare.yml`
13-
- Tooling: React 19, Vite 7 (`rolldown-vite`), Tailwind 4, Vitest 3, Remotion 4, i18next `^25.7.1` (en/fr/ar RTL), Pyodide `0.27.5` in worker, Tone.js `^15.1.22`, DiceBear `^10.3.0`, Phosphor Icons, Monaco Editor
13+
- Tooling: React 19, Vite 7 (`rolldown-vite`), Tailwind 4, Vitest 3, Remotion 4, i18next `^25.7.1` (en/fr/ar RTL), Pyodide `0.27.5` in worker, Tone.js `^15.1.22`, DiceBear `^10.3.0`, Phosphor Icons, Monaco Editor, Framer Motion, Playwright (SFX generation only)
1414
- Engines: Node `>=24.11.1`, pnpm `>=8.15.9` · alias `@/``src/`
15-
- Version: `0.5.0` in `package.json` — 45 algorithms across 5 categories; optional Google sign-in when Supabase env vars are set
15+
- Version: `0.5.0` in `package.json`**45 algorithms** across **5 categories** (14 sorting, 9 pathfinding, 9 searching, 6 tree traversal, 7 graph); optional Google sign-in when Supabase env vars are set
1616
- **PRs target `develop`**, not `main` (gated by `ensure-pr-source-develop.yml`)
17-
- **Tests**: 135 test files, ~1,673 tests
18-
- **Source**: 217 `.js`, 132 `.jsx`, 48 `.py`, 1 `.css`
17+
- **Tests**: 136 test files (~1,700+ tests)
18+
- **Source**: 201 JS/JSX non-test files, 45 Python (one `.py` per algorithm), 1 `.css`
19+
- **Supabase**: `eu-central-1` region, 4 migrations for `profiles` table + RLS; `keep-supabase-alive.yml` prevents free-tier pausing
20+
21+
## Routes
22+
23+
| Path | Page | Auth |
24+
|------|------|------|
25+
| `/` | LandingPage | Public |
26+
| `/app` | VisualizerApp | Public |
27+
| `/roadmap` | Roadmap | Public |
28+
| `/privacy` | PrivacyPolicy (LegalDocument) | Public |
29+
| `/terms` | TermsOfUse (LegalDocument) | Public |
30+
| `/auth/google/callback` | GoogleAuthCallback | Public (OAuth redirect) |
31+
| `/settings/profile` | ProfileSettingsPage | `RequireAuth` |
1932

2033
## Source Of Truth (registries)
2134

2235
When docs drift, trust runtime config:
2336

24-
- `src/constants/index.js` — categories, algorithm keys, complexity, visual enums
25-
- `src/registry/categoryConfig.js` — category wiring, groups, size controls
37+
- `src/constants/index.js` — categories, algorithm keys, complexity, visual enums, size defaults
38+
- `src/constants/algorithmKnowledge.js` — insight metadata (inventor, year, facts count, YouTube video ID)
39+
- `src/registry/categoryConfig.js` — category wiring, groups, size controls, icon, default algorithm
2640
- `src/registry/visualizerRegistry.js`, `extraVisualizerProps.js`, `videoSceneRegistry.jsx`
2741
- `src/registry/complexityDatasetRegistry.js`, `searchingSubstrate.js`, `graphAlgorithmRegistry.js`
2842
- `src/config/algorithmConfig.js`, `settingsConfig.js`
@@ -31,19 +45,28 @@ When docs drift, trust runtime config:
3145
## Category contracts (non-negotiable)
3246

3347
- **Searching:** array vs node-link via `getSearchingSubstrate()` — do not special-case elsewhere
34-
- **Graph algorithms:** node-link vs matrix profiles; Floyd-Warshall matrix max 6 nodes; scenarios from `graphTestScenarios.js`
35-
- **Pathfinding:** grid steps; walls/start/end in `usePathfindingVisualization`
36-
- **Tree:** `generateTreeForTraversal()`; `treeNodeCount` 3–31
37-
- **Sound:** semantic events in `soundEvents.js` only — not from localized descriptions; visualization-only (no UI click sounds)
38-
- **Export:** interactive + Remotion parity; `buildExportSoundCues()` from same sound events
48+
- **Graph algorithms:** node-link vs matrix profiles; Floyd-Warshall matrix max 6 nodes; scenarios from `graphTestScenarios.js` (18 preset scenarios); `GraphScenarioDropdown` when scenario is active
49+
- **Pathfinding:** grid steps; walls/start/end in `usePathfindingVisualization`; grid size uses named presets (`GRID_SIZES.SMALL/MEDIUM/LARGE`)
50+
- **Tree:** `generateTreeForTraversal()`; `treeNodeCount` 3–31, default 15; BST values assigned inorder
51+
- **Sound:** semantic events in `soundEvents.js` only (16 event kinds) — not from localized descriptions; visualization-only (no UI click sounds); uses Tone.js singleton `soundManager` with 5 synths through master chain (gain → filter → compressor → reverb)
52+
- **Export:** interactive + Remotion parity; `buildExportSoundCues()` from same sound events; WAV assets in `public/video-export/sfx/` (18 pre-rendered files)
53+
- **Feature gating:** Tiered access model — see Auth contracts for Anonymous vs Free tier access; `SignInPromptModal` blocks gated features; `entitlementService.js` is the single authority for all access checks; `src/constants/algorithmEntitlements.js` defines the anonymous-tier algorithm allowlist
3954

4055
## Visualizer UX
4156

4257
- Registry-driven architecture — extend config/registries, not `VisualizerApp` one-offs
58+
- All 5 category hooks called unconditionally in `VisualizerApp` (Rules of Hooks), merged via `useCategoryVisualizations()`
4359
- Completion: brief final state before `ComplexityPanel`
4460
- Regenerate control is category-neutral ("Generate New Input")
4561
- Sorting only category with sort-order controls
46-
- Lazy panels (`PythonCodePanel`, `AlgorithmInsightPanel`) stay optional overlays
62+
- Lazy panels (`PythonCodePanel`, `AlgorithmInsightPanel`) stay optional overlays — Monaco editor, Pyodide worker execution, editable test cases, LeetCode-style pass/fail
63+
- Full-screen mode (`useFullScreen`) uses same `ControlPanel` + visualizer registry; F key toggle, Escape exits
64+
- Touch: horizontal swipe (`useSwipe`) for manual step navigation; one-time `SwipeTutorial` on mobile
65+
- Video export: horizontal (1920×1080) or vertical (1080×1920) MP4 via `@remotion/web-renderer`; orientation → capability check → render → preview → download
66+
- Complexity panel: interactive SVG chart with log/linear toggle; best/average/worst time + space complexity
67+
- Graph algorithms: `GraphAlgorithmCategoryVisualizer` routes node-link to `GraphVisualizer` and matrix (Floyd-Warshall) to `GraphAlgorithmMatrixVisualizer`; `GraphScenarioDropdown` for preset scenarios
68+
- Searching category: `SearchingCategoryVisualizer` routes array-based to `ArrayVisualizer` and node-link (DFS/BFS graph) to `GraphVisualizer`
69+
- Animation speeds: SLOW 8000ms, MEDIUM 4800ms, FAST 2400ms, VERY_FAST 1200ms
4770

4871
## Ship It test ladder
4972

@@ -52,32 +75,70 @@ When docs drift, trust runtime config:
5275
| Iterate | `pnpm vitest run <touched tests>` |
5376
| Gate | `pnpm lint``pnpm format:check``pnpm test:coverage``pnpm build` |
5477

55-
Follow `~/.cursor/skills/ship-it/SKILL.md` when user says "ship it".
56-
5778
## i18n / user-facing changes
5879

59-
Audit all three locales + pseudocode strings + export fallbacks when renaming categories or algorithms. Arabic is RTL — check app UI and Remotion export.
80+
Audit all three locales (en/fr/ar) + pseudocode strings + export fallbacks when renaming categories or algorithms. Arabic is RTL — check app UI and Remotion export. Translation detection order: `localStorage``navigator``htmlTag`, cached in localStorage. Pseudocode is English source of truth, FR/AR generated via `localize.js`.
6081

6182
## Adding algorithms or categories
6283

63-
See reference doc for full checklists (JS, Python, pseudocode, sound, insight, tests, registries). Keep graph algorithm commits scoped to one algorithm when possible.
84+
See reference doc for full checklists (JS, Python, pseudocode, sound, insight, tests, registries). Keep graph algorithm commits scoped to one algorithm when possible. Algorithms follow dual-export pattern: visualization function → `steps[]` array + `*Pure` function for testing.
6485

6586
## Auth contracts (non-negotiable)
6687

6788
- **OIDC only** — Google sign-in via Supabase Auth; no email/password flows in v0.5.0
68-
- **Service layer**`src/services/authService.js`, `profileService.js`; components use `AuthContext` / `useAuth`, never import Supabase directly
89+
- **OAuth UX** — Google Identity Services (PKCE popup on `/auth/google/callback`); web uses `signInWithIdToken`, not `signInWithOAuth`; `googleIdentity.js` manages GIS script loading, nonce creation, and popup flow
90+
- **Service layer**`src/services/authService.js`, `profileService.js`, `entitlementService.js`, `googleTokenExchange.js`; components use `AuthContext` / `useAuth`, never import Supabase directly
6991
- **Postgres-portable schema**`profiles` keyed to `auth.users`; RLS on public tables; client-writable columns: `display_name`, `avatar_preference` only; `avatar_url` is OAuth/trigger-populated (not client-writable); `plan` and future `referral_*` / `pro_*` columns are service role / webhook only
70-
- **Profile settings** — private route `/settings/profile` (`RequireAuth`); `updateProfile()` in `profileService.js`; security boundary = RLS row scope + `REVOKE UPDATE` + `GRANT UPDATE (display_name, avatar_preference)`; no public profile route or `username` in v0.5.x
71-
- **Session**`getSession()`, `onAuthStateChange()`; `AuthProvider` in `src/main.jsx`
72-
- **OAuth UX** — Google Identity Services (PKCE popup on `/auth/google/callback`); web uses `signInWithIdToken`, not `signInWithOAuth`
73-
- **i18n** — sign-in/out strings and legal copy in en/fr/ar; audit RTL for Header auth control
74-
- **Free by default** — core visualization is free; Code Panel, Insight Panel, Video Export, Sound, and Fullscreen require sign-in; auth PRs must not reduce free functionality
92+
- **Profile settings** — private route `/settings/profile` (`RequireAuth`); `updateProfile()` in `profileService.js`; security boundary = RLS row scope + `REVOKE UPDATE` + `GRANT UPDATE (display_name, avatar_preference)`; tabbed UI with profile/notifications/connections tabs; DiceBear notionists avatar fallback
93+
- **Session**`getSession()`, `onAuthStateChange()`; `AuthProvider` in `src/main.jsx`; request-dedup via `requestRef`
94+
- **Tiered access model** — Anonymous (no account) gets limited access to drive sign-in conversion; Free account (Google sign-in) unlocks the full platform
95+
- **Anonymous tier (no account):**
96+
- 18 of 45 algorithms (curated starter set across all 5 categories; see `src/constants/algorithmEntitlements.js`)
97+
- 12 visualizations per session (localStorage counter `anon_viz_count`, resets on sign-in)
98+
- Autoplay only, default speed (MEDIUM: 4800ms)
99+
- Complexity panel: 2 views per completion (localStorage `anon_complexity_views`), then blur overlay + sign-in gate
100+
- No manual controls, speed adjustment, or category-specific controls (grid size locked to MEDIUM, sort order locked to ascending, graph scenarios disabled)
101+
- No Code Panel, Insight Panel, Video Export, Sound, or Fullscreen
102+
- **Free tier (Google sign-in):**
103+
- All 45 algorithms, unlimited visualizations
104+
- Manual controls, all 4 speed presets
105+
- Full complexity panel access, all category-specific controls
106+
- Code Panel, Insight Panel, Sound, Fullscreen
107+
- Video Export: unlimited for Free tier with mandatory watermark; Pro tier adds watermark customization/removal; internal daily abuse guard exists (not user-facing)
108+
- **Auth PRs must not reduce Free tier functionality** — only Anonymous tier can be further restricted
75109
- **Secrets** — publishable anon key via `VITE_*` only; service role key never in repo or client bundle
76-
- **CSP** — Supabase origin in `connect-src`; Google profile photos in `img-src`; assert via `scripts/cspHeaders.js`
77-
- **Tests** — mock Supabase in Vitest (`src/test/supabaseMock.js`, wired in `src/test/setup.js`)
110+
- **CSP** — Supabase origin in `connect-src`; Google profile photos in `img-src` (lh3.googleusercontent.com); GIS scripts/connect/frame in respective directives; asserted via `scripts/cspHeaders.js` at build time
111+
- **Tests** — mock Supabase in Vitest (`src/test/supabaseMock.js`, wired in `src/test/setup.js`); mock includes `authStateChangeCallbackRef` for testing auth state changes
112+
- **Auth pages:** `GoogleAuthCallback.jsx` (handles OAuth redirect), `PrivacyPolicy.jsx`, `TermsOfUse.jsx` (both use shared `LegalDocument` component with content from `src/content/legal/`)
78113

79114
## Workflow rules
80115

81116
- Minimal diff; no unrelated edits; preserve user-authored changes
82117
- Sync registries across JS, Python, pseudocode, sound, export, and tests
83118
- Run focused tests while iterating; full gate before Ship It handoff
119+
- Generators should accept `rng` param for deterministic test output
120+
- Sound is silent on initial load, reset, step-back, algorithm changes, and regeneration — only forward playback and manual stepping
121+
- Do NOT add UI click sounds (panels, buttons, toggles, export) — visualization steps only
122+
- When adding net-new visual step states, add corresponding `SOUND_EVENT_KINDS` entry and align `buildExportSoundCues.test.js`
123+
- When adding a graph algorithm, keep commits scoped to one algorithm; update `graphAlgorithmRegistry.js` profile, `graphAlgorithmGenerators.js` if new generator needed, and `graphTestScenarios.js` if new scenarios needed
124+
125+
## Build-time env vars
126+
127+
| Variable | Required | Purpose |
128+
|----------|----------|---------|
129+
| `VITE_GIT_BRANCH` | CI | Deploy context; `isProductionMainBranch()` gates noindex/robots |
130+
| `VITE_SUPABASE_URL` | Auth | Supabase project URL |
131+
| `VITE_SUPABASE_ANON_KEY` | Auth | Publishable anon key |
132+
| `VITE_GOOGLE_WEB_CLIENT_ID` | Auth | Google OAuth client ID |
133+
| `VITE_PYODIDE_CDN_BASE` | Optional | Override default jsDelivr CDN for Pyodide |
134+
| `VITE_DEV_SITE_URL` | CI | Dev site URL for build metadata |
135+
136+
## CI workflow summary
137+
138+
- `ci.yml` — push/PR to main/develop: quality (lint + format + audit) → test (coverage + Codecov) → build (with `VITE_GIT_BRANCH` + `VITE_DEV_SITE_URL`)
139+
- `deploy-cloudflare.yml` — triggered by CI completion on main/develop: build + `wrangler deploy` (production/staging envs)
140+
- `preview-cloudflare.yml` — PR to develop: lint → test → build → `wrangler versions upload` → comment PR with preview URL + QR code; cleanup on PR close
141+
- `ensure-pr-source-develop.yml` — blocks PRs to `main` unless head is `develop` or user in `ALLOWED_MERGERS`
142+
- `release.yml` — GitHub release on `v*` tags
143+
- `keep-supabase-alive.yml` — weekly ping to prevent free-tier project hibernation
144+
- `stale.yml`, `labeler.yml`, `semgrep.yml` — repo hygiene + SAST scanning

docs/AGENTS_REFERENCE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ Future (v0.6.0, not shipped): `username` (unique, set-once RLS), public `/u/:use
250250

251251
- `public/manifest.json`, `public/sitemap.xml`, `public/robots.txt`, `public/logo.svg`
252252
- `public/_headers` — security headers, cache rules, and `workers.dev` noindex for Cloudflare Workers static assets
253+
- `public/ui/sfx/` — interactive UI one-shot sounds (e.g. theme toggle); loaded via Web Audio in `src/utils/themeSwitchSound.js`
254+
- `public/video-export/sfx/` — 18 pre-rendered Remotion export WAV files (see Export sound contract)
253255

254256
### Build, CI, and repo config
255257

public/ui/sfx/switch-on.mp3

1.72 KB
Binary file not shown.

src/components/AlgorithmDropdown.jsx

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
*/
66

77
import { motion, AnimatePresence } from 'framer-motion';
8-
import { Check, CaretDown } from '@phosphor-icons/react';
8+
import { Check, CaretDown, Lock } from '@phosphor-icons/react';
99
import { useTranslation } from 'react-i18next';
10+
import { canAccessAlgorithm } from '@/services/entitlementService';
1011

1112
function AlgorithmDropdown({
1213
algorithms,
@@ -17,6 +18,9 @@ function AlgorithmDropdown({
1718
setIsDropdownOpen,
1819
isPlaying,
1920
dropdownRef,
21+
user,
22+
categoryType,
23+
onLockedAlgorithmClick,
2024
}) {
2125
const { t } = useTranslation();
2226

@@ -72,26 +76,49 @@ function AlgorithmDropdown({
7276
.slice(0, groupIndex)
7377
.reduce((acc, g) => acc + g.algorithms.length, 0) + index;
7478

79+
const isLocked =
80+
categoryType &&
81+
!canAccessAlgorithm(algo.value, categoryType, user);
82+
const isSelected = selectedAlgorithm === algo.value;
83+
7584
return (
7685
<motion.button
7786
key={algo.value}
78-
onClick={() => onAlgorithmSelect(algo.value)}
87+
onClick={() => {
88+
if (isLocked) {
89+
onLockedAlgorithmClick?.(algo);
90+
} else {
91+
onAlgorithmSelect(algo.value);
92+
}
93+
}}
7994
initial={{ opacity: 0, x: -20 }}
8095
animate={{ opacity: 1, x: 0 }}
8196
transition={{ delay: itemIndex * 0.03 }}
8297
className={`w-full px-4 py-3 text-left flex items-center justify-between transition-colors duration-150 hover:bg-surface-elevated ${
83-
selectedAlgorithm === algo.value
84-
? 'bg-theme-primary-light text-theme-primary'
85-
: 'text-text-primary'
98+
isSelected
99+
? 'bg-theme-primary-light text-theme-primary dark:text-white'
100+
: isLocked
101+
? 'text-text-primary opacity-60'
102+
: 'text-text-primary'
86103
}`}
87104
>
88-
<div className="flex flex-col">
89-
<span className="font-medium">{algo.label}</span>
90-
<span className="text-xs text-text-secondary mt-0.5">
105+
<div className="flex flex-col min-w-0 flex-1">
106+
<span
107+
className={`font-medium ${isSelected ? 'text-theme-primary dark:text-white' : ''}`}
108+
>
109+
{algo.label}
110+
</span>
111+
<span
112+
className={`text-xs mt-0.5 ${
113+
isSelected
114+
? 'text-text-secondary dark:text-white/80'
115+
: 'text-text-secondary'
116+
}`}
117+
>
91118
{t('settings.time')}: {algo.complexity}
92119
</span>
93120
</div>
94-
{selectedAlgorithm === algo.value && (
121+
{isSelected ? (
95122
<motion.div
96123
initial={{ scale: 0 }}
97124
animate={{ scale: 1 }}
@@ -100,14 +127,22 @@ function AlgorithmDropdown({
100127
stiffness: 500,
101128
damping: 25,
102129
}}
130+
className="shrink-0"
103131
>
104132
<Check
105133
size={18}
106134
weight="bold"
107-
className="text-[#3b82f6]"
135+
className="text-[#3b82f6] dark:text-white"
108136
/>
109137
</motion.div>
110-
)}
138+
) : isLocked ? (
139+
<Lock
140+
size={18}
141+
weight="bold"
142+
className="shrink-0 text-text-tertiary"
143+
aria-hidden="true"
144+
/>
145+
) : null}
111146
</motion.button>
112147
);
113148
})}

0 commit comments

Comments
 (0)