From 3e6386470286bf1bcbcb77e207db8950cd707aff Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Wed, 21 Jan 2026 04:02:58 -0300 Subject: [PATCH] feat: [US-007] - Sign up page (invite-only) Co-Authored-By: Claude Opus 4.5 --- prd.json | 12 +- progress.txt | 92 +++++++++++++++ src/app/signup/page.tsx | 243 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 341 insertions(+), 6 deletions(-) create mode 100644 src/app/signup/page.tsx diff --git a/prd.json b/prd.json index 8ec99e1..694e04c 100644 --- a/prd.json +++ b/prd.json @@ -35,7 +35,7 @@ "Typecheck passes" ], "priority": 2, - "passes": false, + "passes": true, "notes": "" }, { @@ -52,7 +52,7 @@ "Typecheck passes" ], "priority": 3, - "passes": false, + "passes": true, "notes": "" }, { @@ -67,7 +67,7 @@ "Typecheck passes" ], "priority": 4, - "passes": false, + "passes": true, "notes": "" }, { @@ -83,7 +83,7 @@ "Typecheck passes" ], "priority": 5, - "passes": false, + "passes": true, "notes": "" }, { @@ -102,7 +102,7 @@ "Verify in browser using dev-browser skill" ], "priority": 6, - "passes": false, + "passes": true, "notes": "" }, { @@ -121,7 +121,7 @@ "Verify in browser using dev-browser skill" ], "priority": 7, - "passes": false, + "passes": true, "notes": "" }, { diff --git a/progress.txt b/progress.txt index bdb7d94..0737ad9 100644 --- a/progress.txt +++ b/progress.txt @@ -3,6 +3,16 @@ - Source files are in `src/` directory (app, components, lib, types) - Supabase is configured with @supabase/supabase-js and @supabase/ssr packages - Environment variables follow NEXT_PUBLIC_* convention for client-side access +- Use `npm run typecheck` to run TypeScript type checking (tsc --noEmit) +- Flowchart types exported from `src/types/flowchart.ts` +- Supabase migrations go in `supabase/migrations/` with timestamp prefix (YYYYMMDDHHMMSS_*.sql) +- Database has profiles table (linked to auth.users) and projects table (with flowchart_data JSONB) +- RLS policies enforce user_id = auth.uid() for project access +- Supabase client utilities in `src/lib/supabase/`: client.ts (browser), server.ts (App Router), middleware.ts (route protection) +- Next.js middleware.ts at project root handles route protection using updateSession helper +- Public auth routes: /login, /signup, /forgot-password, /reset-password +- Protected routes: /dashboard, /editor/* (redirect to /login if unauthenticated) +- Auth pages use 'use client' with useState, createClient() from lib/supabase/client.ts, and useRouter for redirects --- @@ -26,3 +36,85 @@ - npm package names can't have capital letters (use lowercase) - .gitignore needs explicit exclusion for .env files, but include .env.example --- + +## 2026-01-21 - US-002 +- What was implemented: TypeScript types for flowchart data structures +- Files changed: + - src/types/flowchart.ts - new file with all flowchart type definitions + - package.json - added typecheck script (tsc --noEmit) +- **Learnings for future iterations:** + - Position is a helper type for {x, y} coordinates used by nodes + - FlowchartNode is a union type of DialogueNode | ChoiceNode | VariableNode + - ChoiceOption is a separate type to make options array cleaner + - All types use `export type` for TypeScript isolatedModules compatibility +--- + +## 2026-01-21 - US-003 +- What was implemented: Supabase schema for users and projects +- Files changed: + - supabase/migrations/20260121000000_create_profiles_and_projects.sql - new file with all database schema +- **Learnings for future iterations:** + - Supabase migrations are plain SQL files in supabase/migrations/ directory + - Migration filenames use timestamp prefix (YYYYMMDDHHMMSS_description.sql) + - RLS policies need separate policies for SELECT, INSERT, UPDATE, DELETE operations + - Admin check policy uses EXISTS subquery to check is_admin flag on profiles table + - projects table references profiles.id (not auth.users.id directly) for proper FK relationships + - flowchart_data column uses JSONB type with default empty structure + - Added auto-update trigger for updated_at timestamp on projects table +--- + +## 2026-01-21 - US-004 +- What was implemented: Supabase client configuration utilities +- Files changed: + - src/lib/supabase/client.ts - browser client using createBrowserClient from @supabase/ssr + - src/lib/supabase/server.ts - server client for App Router with async cookies() API + - src/lib/supabase/middleware.ts - middleware helper with updateSession function + - src/lib/.gitkeep - removed (no longer needed) +- **Learnings for future iterations:** + - @supabase/ssr package provides createBrowserClient and createServerClient functions + - Server client requires async cookies() from next/headers in Next.js 16 + - Middleware client returns both user object and supabaseResponse for route protection + - Cookie handling uses getAll/setAll pattern for proper session management + - setAll in server.ts wrapped in try/catch to handle Server Component limitations +--- + +## 2026-01-21 - US-005 +- What was implemented: Protected routes middleware for authentication +- Files changed: + - middleware.ts - new file at project root for route protection +- **Learnings for future iterations:** + - Next.js middleware.ts must be at project root (not in src/) + - updateSession helper from lib/supabase/middleware.ts returns { user, supabaseResponse } + - Use startsWith() for route matching to handle nested routes (e.g., /editor/*) + - Matcher config excludes static files and images to avoid unnecessary middleware calls + - Clone nextUrl before modifying pathname for redirects +--- + +## 2026-01-21 - US-006 +- What was implemented: Login page with email/password authentication +- Files changed: + - src/app/login/page.tsx - new file with login form and Supabase auth +- **Learnings for future iterations:** + - Auth pages use 'use client' directive since they need useState and form handling + - Use createClient() from lib/supabase/client.ts for browser-side auth operations + - supabase.auth.signInWithPassword returns { error } object for handling failures + - useRouter from next/navigation for programmatic redirects after auth + - Error state displayed in red alert box with dark mode support + - Loading state disables submit button and shows "Signing in..." text + - TailwindCSS dark mode uses dark: prefix (e.g., dark:bg-zinc-950) +--- + +## 2026-01-21 - US-007 +- What was implemented: Sign up page for invite-only account setup +- Files changed: + - src/app/signup/page.tsx - new file with signup form and Supabase auth +- **Learnings for future iterations:** + - Supabase invite tokens come via URL hash fragment (window.location.hash) + - Parse hash with URLSearchParams after removing leading '#' + - Check for type=invite or type=signup to detect invite flow + - Use setSession() with access_token and refresh_token to establish session from invite link + - For invited users, update password with updateUser() then create profile with upsert() + - Use upsert() instead of insert() for profiles to handle edge cases + - Validate password confirmation before submission (passwords match check) + - display_name defaults to email prefix (split('@')[0]) +--- diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx new file mode 100644 index 0000000..da9c15b --- /dev/null +++ b/src/app/signup/page.tsx @@ -0,0 +1,243 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import Link from 'next/link' +import { createClient } from '@/lib/supabase/client' + +export default function SignupPage() { + const router = useRouter() + const searchParams = useSearchParams() + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [error, setError] = useState(null) + const [loading, setLoading] = useState(false) + + useEffect(() => { + // Pre-fill email if provided in URL (from invite link) + const emailParam = searchParams.get('email') + if (emailParam) { + setEmail(emailParam) + } + + // Handle invite/signup token from URL hash + // Supabase adds tokens to the URL hash after redirect + const handleTokenFromUrl = async () => { + const hash = window.location.hash + if (hash) { + const params = new URLSearchParams(hash.substring(1)) + const accessToken = params.get('access_token') + const refreshToken = params.get('refresh_token') + const type = params.get('type') + + if (accessToken && refreshToken && (type === 'invite' || type === 'signup')) { + const supabase = createClient() + const { error } = await supabase.auth.setSession({ + access_token: accessToken, + refresh_token: refreshToken, + }) + + if (error) { + setError('Invalid or expired invite link. Please request a new invitation.') + return + } + + // Get the user's email from the session + const { data: { user } } = await supabase.auth.getUser() + if (user?.email) { + setEmail(user.email) + } + } + } + } + + handleTokenFromUrl() + }, [searchParams]) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError(null) + + // Validate passwords match + if (password !== confirmPassword) { + setError('Passwords do not match') + return + } + + // Validate password length + if (password.length < 6) { + setError('Password must be at least 6 characters') + return + } + + setLoading(true) + + const supabase = createClient() + + // Check if user already has a session (from invite link) + const { data: { session } } = await supabase.auth.getSession() + + if (session) { + // User was invited and has a session - update their password + const { error: updateError } = await supabase.auth.updateUser({ + password, + }) + + if (updateError) { + setError(updateError.message) + setLoading(false) + return + } + + // Create profile record + const { error: profileError } = await supabase.from('profiles').upsert({ + id: session.user.id, + email: session.user.email, + display_name: session.user.email?.split('@')[0] || 'User', + is_admin: false, + }) + + if (profileError) { + setError(profileError.message) + setLoading(false) + return + } + + router.push('/dashboard') + } else { + // Regular signup flow (if allowed) + const { data, error } = await supabase.auth.signUp({ + email, + password, + }) + + if (error) { + setError(error.message) + setLoading(false) + return + } + + if (data.user) { + // Create profile record + const { error: profileError } = await supabase.from('profiles').upsert({ + id: data.user.id, + email: data.user.email, + display_name: email.split('@')[0] || 'User', + is_admin: false, + }) + + if (profileError) { + setError(profileError.message) + setLoading(false) + return + } + + router.push('/dashboard') + } + } + } + + return ( +
+
+
+

+ WebVNWrite +

+

+ Complete your account setup +

+
+ +
+ {error && ( +
+

{error}

+
+ )} + +
+
+ + setEmail(e.target.value)} + className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" + placeholder="you@example.com" + /> +
+ +
+ + setPassword(e.target.value)} + className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" + placeholder="••••••••" + /> +
+ +
+ + setConfirmPassword(e.target.value)} + className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" + placeholder="••••••••" + /> +
+
+ + + +

+ Already have an account?{' '} + + Sign in + +

+
+
+
+ ) +}