diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0d2f7a580..1e26f5ebd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import { FilesModalProvider } from './contexts/FilesModalContext'; import { AuthProvider } from './lib/useSession'; import HomePage from './pages/HomePage'; import LoginCompact from './routes/LoginCompact'; +import Signup from './routes/Signup'; import AuthCallback from './routes/AuthCallback'; import AuthDebug from './routes/AuthDebug'; @@ -22,6 +23,7 @@ export default function App() { } /> } /> + } /> } /> } /> {/* Catch-all route - redirects unknown paths to home */} diff --git a/frontend/src/routes/AuthDebug.tsx b/frontend/src/routes/AuthDebug.tsx index 9280f1f08..af13338e6 100644 --- a/frontend/src/routes/AuthDebug.tsx +++ b/frontend/src/routes/AuthDebug.tsx @@ -13,6 +13,11 @@ export default function AuthDebug() { const [apiRequestBody, setApiRequestBody] = useState('') const [apiResponse, setApiResponse] = useState(null) const [isTestingApi, setIsTestingApi] = useState(false) + + // Admin functions state + const [inviteEmail, setInviteEmail] = useState('') + const [newEmail, setNewEmail] = useState('') + const [isProcessingAdmin, setIsProcessingAdmin] = useState(false) const runAuthTests = async () => { setIsTestingAuth(true) @@ -203,6 +208,94 @@ export default function AuthDebug() { } } + const inviteUser = async () => { + if (!inviteEmail || !session) { + alert('Please enter an email and ensure you are signed in') + return + } + + // Show information about service role requirement + const proceed = confirm( + `⚠️ Admin Invite requires SERVICE ROLE permissions.\n\n` + + `This will likely fail unless you:\n` + + `1. Have a service role key configured\n` + + `2. Are using RLS bypass\n` + + `3. Have admin privileges\n\n` + + `Alternative: Use the Magic Link feature instead.\n\n` + + `Continue anyway?` + ) + + if (!proceed) return + + try { + setIsProcessingAdmin(true) + console.log('[Admin Debug] Inviting user:', inviteEmail) + + // Note: This requires admin/service role permissions + const { data, error } = await supabase.auth.admin.inviteUserByEmail( + inviteEmail.trim(), + { redirectTo: `${window.location.origin}/welcome` } + ) + + if (error) { + console.error('[Admin Debug] Invite error:', error) + + // Provide helpful error message + let errorMsg = error.message + if (error.message.includes('Bearer token') || error.message.includes('service role')) { + errorMsg = `❌ Service Role Required\n\n` + + `The invite function requires a service role key, not a user JWT.\n\n` + + `Solutions:\n` + + `• Use Magic Link instead (works with user permissions)\n` + + `• Configure service role in backend\n` + + `• Use Supabase Dashboard → Authentication → Users → Invite\n\n` + + `Original error: ${error.message}` + } + + alert(errorMsg) + } else { + console.log('[Admin Debug] Invite successful:', data) + alert(`✅ Invitation sent to ${inviteEmail}!`) + setInviteEmail('') + } + } catch (err) { + console.error('[Admin Debug] Invite unexpected error:', err) + alert(`Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`) + } finally { + setIsProcessingAdmin(false) + } + } + + const changeEmail = async () => { + if (!newEmail || !session) { + alert('Please enter a new email and ensure you are signed in') + return + } + + try { + setIsProcessingAdmin(true) + console.log('[Admin Debug] Changing email to:', newEmail) + + const { data, error } = await supabase.auth.updateUser({ + email: newEmail.trim() + }) + + if (error) { + console.error('[Admin Debug] Email change error:', error) + alert(`Failed to change email: ${error.message}`) + } else { + console.log('[Admin Debug] Email change initiated:', data) + alert(`Email change confirmation sent to both ${user?.email} and ${newEmail}. Check both inboxes for confirmation links.`) + setNewEmail('') + } + } catch (err) { + console.error('[Admin Debug] Email change unexpected error:', err) + alert(`Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`) + } finally { + setIsProcessingAdmin(false) + } + } + return (
@@ -539,6 +632,145 @@ export default function AuthDebug() {
)} + {/* Admin Functions */} + {session && ( +
+

+ 👑 Admin Functions +

+

+ Test admin-level authentication features (requires appropriate permissions) +

+ +
+ {/* Invite User */} +
+

📨 Invite User

+
+
+ + setInviteEmail(e.target.value)} + placeholder="user@example.com" + className="w-full px-3 py-2 border border-blue-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ + +
+
+
⚠️ Admin Invite: Requires service role key (will likely fail from frontend)
+
✅ Magic Link: Works with user permissions, allows account creation
+
Alternative: Use Supabase Dashboard → Authentication → Users → Invite
+
+
+
+ + {/* Change Email */} +
+

📧 Change Email Address

+
+
+ + setNewEmail(e.target.value)} + placeholder="new-email@example.com" + className="w-full px-3 py-2 border border-orange-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500" + /> +
+ +
+
Current Email: {user?.email}
+
Note: Sends confirmation emails to both old and new addresses.
+
+
+
+ + {/* Quick Admin Actions */} +
+

⚡ Quick Actions

+
+ + + +
+
+
+
+ )} + {/* Test Results */} {testResults && (
diff --git a/frontend/src/routes/LoginCompact.tsx b/frontend/src/routes/LoginCompact.tsx index 5116e5a4f..abda78187 100644 --- a/frontend/src/routes/LoginCompact.tsx +++ b/frontend/src/routes/LoginCompact.tsx @@ -8,6 +8,13 @@ export default function LoginCompact() { const { session, user, loading, signOut } = useAuth() const [isSigningIn, setIsSigningIn] = useState(false) const [error, setError] = useState(null) + const [showEmailForm, setShowEmailForm] = useState(false) + const [showMagicLink, setShowMagicLink] = useState(false) + const [showPasswordReset, setShowPasswordReset] = useState(false) + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [magicLinkEmail, setMagicLinkEmail] = useState('') + const [resetEmail, setResetEmail] = useState('') // Show logged in state if authenticated if (session && !loading) { @@ -130,6 +137,108 @@ export default function LoginCompact() { } } + const signInWithEmail = async () => { + if (!email || !password) { + setError('Please enter both email and password') + return + } + + try { + setIsSigningIn(true) + setError(null) + + console.log('[LoginCompact] Signing in with email:', email) + + const { data, error } = await supabase.auth.signInWithPassword({ + email: email.trim(), + password: password + }) + + if (error) { + console.error('[LoginCompact] Email sign in error:', error) + setError(error.message) + } else if (data.user) { + console.log('[LoginCompact] Email sign in successful') + // User will be redirected by the auth state change + } + } catch (err) { + console.error('[LoginCompact] Unexpected error:', err) + setError(`Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`) + } finally { + setIsSigningIn(false) + } + } + + const signInWithMagicLink = async () => { + if (!magicLinkEmail) { + setError('Please enter your email address') + return + } + + try { + setIsSigningIn(true) + setError(null) + + console.log('[LoginCompact] Sending magic link to:', magicLinkEmail) + + const { error } = await supabase.auth.signInWithOtp({ + email: magicLinkEmail.trim(), + options: { + emailRedirectTo: `${window.location.origin}/auth/callback` + } + }) + + if (error) { + console.error('[LoginCompact] Magic link error:', error) + setError(error.message) + } else { + setError(null) + alert(`Magic link sent to ${magicLinkEmail}! Check your email and click the link to sign in.`) + setMagicLinkEmail('') + setShowMagicLink(false) + } + } catch (err) { + console.error('[LoginCompact] Unexpected error:', err) + setError(`Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`) + } finally { + setIsSigningIn(false) + } + } + + const resetPassword = async () => { + if (!resetEmail) { + setError('Please enter your email address') + return + } + + try { + setIsSigningIn(true) + setError(null) + + console.log('[LoginCompact] Sending password reset to:', resetEmail) + + const { error } = await supabase.auth.resetPasswordForEmail( + resetEmail.trim(), + { redirectTo: `${window.location.origin}/auth/reset` } + ) + + if (error) { + console.error('[LoginCompact] Password reset error:', error) + setError(error.message) + } else { + setError(null) + alert(`Password reset link sent to ${resetEmail}! Check your email and follow the instructions.`) + setResetEmail('') + setShowPasswordReset(false) + } + } catch (err) { + console.error('[LoginCompact] Unexpected error:', err) + setError(`Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`) + } finally { + setIsSigningIn(false) + } + } + if (loading) { return (
)} - {/* Buttons */} -
+ {/* Email/Password Form */} + {showEmailForm ? ( +
+
+ setEmail(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && !isSigningIn && signInWithEmail()} + style={{ + width: '100%', + padding: '12px 16px', + border: '1px solid #d1d5db', + borderRadius: '8px', + fontSize: '14px', + backgroundColor: '#ffffff', + boxSizing: 'border-box' + }} + /> + setPassword(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && !isSigningIn && signInWithEmail()} + style={{ + width: '100%', + padding: '12px 16px', + border: '1px solid #d1d5db', + borderRadius: '8px', + fontSize: '14px', + backgroundColor: '#ffffff', + boxSizing: 'border-box' + }} + /> +
+ + +
+
+ +
+
+
+ ) : showMagicLink ? ( + /* Magic Link Form */ +
+
+ setMagicLinkEmail(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && !isSigningIn && signInWithMagicLink()} + style={{ + width: '100%', + padding: '12px 16px', + border: '1px solid #d1d5db', + borderRadius: '8px', + fontSize: '14px', + backgroundColor: '#ffffff', + boxSizing: 'border-box' + }} + /> +
+ + +
+
+ We'll send you a secure link to sign in without a password +
+
+
+ ) : showPasswordReset ? ( + /* Password Reset Form */ +
+
+ setResetEmail(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && !isSigningIn && resetPassword()} + style={{ + width: '100%', + padding: '12px 16px', + border: '1px solid #d1d5db', + borderRadius: '8px', + fontSize: '14px', + backgroundColor: '#ffffff', + boxSizing: 'border-box' + }} + /> +
+ + +
+
+ We'll send you instructions to reset your password +
+
+
+ ) : ( + <> + {/* Auth Method Toggles */} +
+ + +
+ + + +
+
+ + {/* Separator */} +
+
+ + or continue with + +
+ + )} + + {/* OAuth Buttons Container */} + {!showEmailForm && ( +
{/* GitHub */} -
+
+ )} {/* Footer */}
(null) + const [success, setSuccess] = useState(null) + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + + const validateForm = () => { + if (!email || !password || !confirmPassword) { + setError('Please fill in all fields') + return false + } + + if (password !== confirmPassword) { + setError('Passwords do not match') + return false + } + + if (password.length < 6) { + setError('Password must be at least 6 characters long') + return false + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(email)) { + setError('Please enter a valid email address') + return false + } + + return true + } + + const signUp = async () => { + if (!validateForm()) return + + try { + setIsSigningUp(true) + setError(null) + setSuccess(null) + + console.log('[Signup] Creating account for:', email) + + const { data, error } = await supabase.auth.signUp({ + email: email.trim(), + password: password, + options: { + emailRedirectTo: `${window.location.origin}/auth/callback` + } + }) + + if (error) { + console.error('[Signup] Sign up error:', error) + setError(error.message) + } else if (data.user) { + console.log('[Signup] Sign up successful:', data.user) + + // Check if email confirmation is required + if (data.user && !data.session) { + setSuccess('Check your email for a confirmation link to complete your registration.') + } else { + setSuccess('Account created successfully! You can now sign in.') + setTimeout(() => navigate('/login'), 2000) + } + } + } catch (err) { + console.error('[Signup] Unexpected error:', err) + setError(`Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`) + } finally { + setIsSigningUp(false) + } + } + + return ( +
+
+ {/* Header */} +
+
🚀
+

+ Create Account +

+

+ Join Stirling PDF to get started +

+
+ + {/* Success Message */} + {success && ( +
+

+ {success} +

+
+ )} + + {/* Error */} + {error && ( +
+

+ {error} +

+
+ )} + + {/* Form */} +
+
+ + setEmail(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && !isSigningUp && signUp()} + style={{ + width: '100%', + padding: '12px 16px', + border: '1px solid #d1d5db', + borderRadius: '8px', + fontSize: '14px', + backgroundColor: '#ffffff', + boxSizing: 'border-box' + }} + /> +
+ +
+ + setPassword(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && !isSigningUp && signUp()} + style={{ + width: '100%', + padding: '12px 16px', + border: '1px solid #d1d5db', + borderRadius: '8px', + fontSize: '14px', + backgroundColor: '#ffffff', + boxSizing: 'border-box' + }} + /> +
+ +
+ + setConfirmPassword(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && !isSigningUp && signUp()} + style={{ + width: '100%', + padding: '12px 16px', + border: '1px solid #d1d5db', + borderRadius: '8px', + fontSize: '14px', + backgroundColor: '#ffffff', + boxSizing: 'border-box' + }} + /> +
+
+ + {/* Sign Up Button */} + + + {/* Sign In Link */} +
+ +
+ + {/* Footer */} +
+

+ By creating an account, you agree to our terms of service +

+
+
+
+ ) +} \ No newline at end of file