mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
email signup
This commit is contained in:
parent
a07f2cbe05
commit
24cabb7070
@ -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() {
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/login" element={<LoginCompact />} />
|
||||
<Route path="/signup" element={<Signup />} />
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
<Route path="/debug" element={<AuthDebug />} />
|
||||
{/* Catch-all route - redirects unknown paths to home */}
|
||||
|
@ -14,6 +14,11 @@ export default function AuthDebug() {
|
||||
const [apiResponse, setApiResponse] = useState<any>(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)
|
||||
setTestResults(null)
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="max-w-4xl mx-auto px-4 space-y-8">
|
||||
@ -539,6 +632,145 @@ export default function AuthDebug() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Admin Functions */}
|
||||
{session && (
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center">
|
||||
👑 Admin Functions
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-6 text-sm">
|
||||
Test admin-level authentication features (requires appropriate permissions)
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Invite User */}
|
||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-md">
|
||||
<h3 className="text-lg font-medium text-blue-900 mb-3">📨 Invite User</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-blue-800 mb-1">
|
||||
Email Address to Invite:
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={inviteEmail}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={inviteUser}
|
||||
disabled={isProcessingAdmin || !inviteEmail}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isProcessingAdmin ? 'Sending Invite...' : 'Try Admin Invite'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (inviteEmail) {
|
||||
supabase.auth.signInWithOtp({
|
||||
email: inviteEmail.trim(),
|
||||
options: { emailRedirectTo: `${window.location.origin}/auth/callback` }
|
||||
}).then(({ error }) => {
|
||||
if (error) alert(`Error: ${error.message}`)
|
||||
else {
|
||||
alert(`✅ Magic link sent to ${inviteEmail}!\n\nThey can use this to create an account and sign in.`)
|
||||
setInviteEmail('')
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
disabled={!inviteEmail}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Send Magic Link Instead
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-xs text-blue-700 space-y-1">
|
||||
<div><strong>⚠️ Admin Invite:</strong> Requires service role key (will likely fail from frontend)</div>
|
||||
<div><strong>✅ Magic Link:</strong> Works with user permissions, allows account creation</div>
|
||||
<div><strong>Alternative:</strong> Use Supabase Dashboard → Authentication → Users → Invite</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Change Email */}
|
||||
<div className="p-4 bg-orange-50 border border-orange-200 rounded-md">
|
||||
<h3 className="text-lg font-medium text-orange-900 mb-3">📧 Change Email Address</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-orange-800 mb-1">
|
||||
New Email Address:
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={newEmail}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={changeEmail}
|
||||
disabled={isProcessingAdmin || !newEmail}
|
||||
className="px-4 py-2 bg-orange-600 text-white rounded hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isProcessingAdmin ? 'Processing...' : 'Change Email'}
|
||||
</button>
|
||||
<div className="text-xs text-orange-700 space-y-1">
|
||||
<div><strong>Current Email:</strong> {user?.email}</div>
|
||||
<div><strong>Note:</strong> Sends confirmation emails to both old and new addresses.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Admin Actions */}
|
||||
<div className="p-4 bg-purple-50 border border-purple-200 rounded-md">
|
||||
<h3 className="text-lg font-medium text-purple-900 mb-3">⚡ Quick Actions</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
const email = prompt('Enter email address for magic link:')
|
||||
if (email) {
|
||||
supabase.auth.signInWithOtp({
|
||||
email: email.trim(),
|
||||
options: { emailRedirectTo: `${window.location.origin}/auth/callback` }
|
||||
}).then(({ error }) => {
|
||||
if (error) alert(`Error: ${error.message}`)
|
||||
else alert(`Magic link sent to ${email}!`)
|
||||
})
|
||||
}
|
||||
}}
|
||||
className="px-3 py-2 bg-purple-600 text-white text-sm rounded hover:bg-purple-700"
|
||||
>
|
||||
🪄 Send Magic Link
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
const email = prompt('Enter email address for password reset:')
|
||||
if (email) {
|
||||
supabase.auth.resetPasswordForEmail(
|
||||
email.trim(),
|
||||
{ redirectTo: `${window.location.origin}/auth/reset` }
|
||||
).then(({ error }) => {
|
||||
if (error) alert(`Error: ${error.message}`)
|
||||
else alert(`Password reset sent to ${email}!`)
|
||||
})
|
||||
}
|
||||
}}
|
||||
className="px-3 py-2 bg-purple-600 text-white text-sm rounded hover:bg-purple-700"
|
||||
>
|
||||
🔑 Reset Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Test Results */}
|
||||
{testResults && (
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
|
@ -8,6 +8,13 @@ export default function LoginCompact() {
|
||||
const { session, user, loading, signOut } = useAuth()
|
||||
const [isSigningIn, setIsSigningIn] = useState(false)
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div style={{
|
||||
@ -205,7 +314,351 @@ export default function LoginCompact() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
{/* Email/Password Form */}
|
||||
{showEmailForm ? (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email address"
|
||||
value={email}
|
||||
onChange={(e) => 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'
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => 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'
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={signInWithEmail}
|
||||
disabled={isSigningIn || !email || !password}
|
||||
style={{
|
||||
flex: '1',
|
||||
padding: '12px 16px',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#059669',
|
||||
color: '#ffffff',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
cursor: isSigningIn || !email || !password ? 'not-allowed' : 'pointer',
|
||||
opacity: isSigningIn || !email || !password ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{isSigningIn ? 'Signing In...' : 'Sign In'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowEmailForm(false)
|
||||
setEmail('')
|
||||
setPassword('')
|
||||
setError(null)
|
||||
}}
|
||||
disabled={isSigningIn}
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#374151',
|
||||
fontSize: '14px',
|
||||
cursor: isSigningIn ? 'not-allowed' : 'pointer',
|
||||
opacity: isSigningIn ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<button
|
||||
onClick={() => navigate('/signup')}
|
||||
disabled={isSigningIn}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#3b82f6',
|
||||
fontSize: '13px',
|
||||
cursor: isSigningIn ? 'not-allowed' : 'pointer',
|
||||
textDecoration: 'underline',
|
||||
opacity: isSigningIn ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
Don't have an account? Sign up
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : showMagicLink ? (
|
||||
/* Magic Link Form */
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Enter your email address"
|
||||
value={magicLinkEmail}
|
||||
onChange={(e) => 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'
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={signInWithMagicLink}
|
||||
disabled={isSigningIn || !magicLinkEmail}
|
||||
style={{
|
||||
flex: '1',
|
||||
padding: '12px 16px',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#7c3aed',
|
||||
color: '#ffffff',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
cursor: isSigningIn || !magicLinkEmail ? 'not-allowed' : 'pointer',
|
||||
opacity: isSigningIn || !magicLinkEmail ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{isSigningIn ? 'Sending...' : 'Send Magic Link'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowMagicLink(false)
|
||||
setMagicLinkEmail('')
|
||||
setError(null)
|
||||
}}
|
||||
disabled={isSigningIn}
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#374151',
|
||||
fontSize: '14px',
|
||||
cursor: isSigningIn ? 'not-allowed' : 'pointer',
|
||||
opacity: isSigningIn ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', fontSize: '12px', color: '#6b7280' }}>
|
||||
We'll send you a secure link to sign in without a password
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : showPasswordReset ? (
|
||||
/* Password Reset Form */
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Enter your email address"
|
||||
value={resetEmail}
|
||||
onChange={(e) => 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'
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={resetPassword}
|
||||
disabled={isSigningIn || !resetEmail}
|
||||
style={{
|
||||
flex: '1',
|
||||
padding: '12px 16px',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#dc2626',
|
||||
color: '#ffffff',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
cursor: isSigningIn || !resetEmail ? 'not-allowed' : 'pointer',
|
||||
opacity: isSigningIn || !resetEmail ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{isSigningIn ? 'Sending...' : 'Reset Password'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowPasswordReset(false)
|
||||
setResetEmail('')
|
||||
setError(null)
|
||||
}}
|
||||
disabled={isSigningIn}
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#374151',
|
||||
fontSize: '14px',
|
||||
cursor: isSigningIn ? 'not-allowed' : 'pointer',
|
||||
opacity: isSigningIn ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', fontSize: '12px', color: '#6b7280' }}>
|
||||
We'll send you instructions to reset your password
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Auth Method Toggles */}
|
||||
<div style={{ marginBottom: '16px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowEmailForm(true)
|
||||
setShowMagicLink(false)
|
||||
setShowPasswordReset(false)
|
||||
setError(null)
|
||||
}}
|
||||
disabled={isSigningIn}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 16px',
|
||||
border: '2px solid #059669',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#f0fdf4',
|
||||
fontSize: '13px',
|
||||
fontWeight: '600',
|
||||
color: '#059669',
|
||||
cursor: isSigningIn ? 'not-allowed' : 'pointer',
|
||||
opacity: isSigningIn ? 0.6 : 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '6px'
|
||||
}}
|
||||
>
|
||||
📧 Email & Password
|
||||
</button>
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowMagicLink(true)
|
||||
setShowEmailForm(false)
|
||||
setShowPasswordReset(false)
|
||||
setError(null)
|
||||
}}
|
||||
disabled={isSigningIn}
|
||||
style={{
|
||||
flex: '1',
|
||||
padding: '10px 16px',
|
||||
border: '2px solid #7c3aed',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#faf5ff',
|
||||
fontSize: '13px',
|
||||
fontWeight: '600',
|
||||
color: '#7c3aed',
|
||||
cursor: isSigningIn ? 'not-allowed' : 'pointer',
|
||||
opacity: isSigningIn ? 0.6 : 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '6px'
|
||||
}}
|
||||
>
|
||||
🪄 Magic Link
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowPasswordReset(true)
|
||||
setShowEmailForm(false)
|
||||
setShowMagicLink(false)
|
||||
setError(null)
|
||||
}}
|
||||
disabled={isSigningIn}
|
||||
style={{
|
||||
flex: '1',
|
||||
padding: '10px 16px',
|
||||
border: '2px solid #dc2626',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#fef2f2',
|
||||
fontSize: '13px',
|
||||
fontWeight: '600',
|
||||
color: '#dc2626',
|
||||
cursor: isSigningIn ? 'not-allowed' : 'pointer',
|
||||
opacity: isSigningIn ? 0.6 : 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '6px'
|
||||
}}
|
||||
>
|
||||
🔑 Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
margin: '16px 0',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '0',
|
||||
right: '0',
|
||||
height: '1px',
|
||||
backgroundColor: '#e5e7eb'
|
||||
}} />
|
||||
<span style={{
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#6b7280',
|
||||
fontSize: '12px',
|
||||
padding: '0 12px'
|
||||
}}>
|
||||
or continue with
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* OAuth Buttons Container */}
|
||||
{!showEmailForm && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{/* GitHub */}
|
||||
<button
|
||||
@ -318,6 +771,7 @@ export default function LoginCompact() {
|
||||
LinkedIn
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
|
301
frontend/src/routes/Signup.tsx
Normal file
301
frontend/src/routes/Signup.tsx
Normal file
@ -0,0 +1,301 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { supabase } from '../lib/supabase'
|
||||
|
||||
export default function Signup() {
|
||||
const navigate = useNavigate()
|
||||
const [isSigningUp, setIsSigningUp] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [success, setSuccess] = useState<string | null>(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 (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#f3f4f6',
|
||||
padding: '16px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: '400px',
|
||||
width: '100%',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '16px',
|
||||
boxShadow: '0 10px 25px rgba(0, 0, 0, 0.1)',
|
||||
padding: '32px'
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
|
||||
<div style={{ fontSize: '40px', marginBottom: '12px' }}>🚀</div>
|
||||
<h1 style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: '600',
|
||||
color: '#1f2937',
|
||||
marginBottom: '8px',
|
||||
margin: '0'
|
||||
}}>
|
||||
Create Account
|
||||
</h1>
|
||||
<p style={{
|
||||
color: '#6b7280',
|
||||
fontSize: '14px',
|
||||
margin: '0'
|
||||
}}>
|
||||
Join Stirling PDF to get started
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Success Message */}
|
||||
{success && (
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
backgroundColor: '#f0fdf4',
|
||||
border: '1px solid #bbf7d0',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '24px'
|
||||
}}>
|
||||
<p style={{
|
||||
color: '#059669',
|
||||
fontSize: '14px',
|
||||
margin: '0'
|
||||
}}>
|
||||
{success}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
backgroundColor: '#fef2f2',
|
||||
border: '1px solid #fecaca',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '24px'
|
||||
}}>
|
||||
<p style={{
|
||||
color: '#dc2626',
|
||||
fontSize: '14px',
|
||||
margin: '0'
|
||||
}}>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', marginBottom: '24px' }}>
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
color: '#374151',
|
||||
marginBottom: '6px'
|
||||
}}>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
value={email}
|
||||
onChange={(e) => 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'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
color: '#374151',
|
||||
marginBottom: '6px'
|
||||
}}>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Minimum 6 characters"
|
||||
value={password}
|
||||
onChange={(e) => 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'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
color: '#374151',
|
||||
marginBottom: '6px'
|
||||
}}>
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Re-enter your password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => 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'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sign Up Button */}
|
||||
<button
|
||||
onClick={signUp}
|
||||
disabled={isSigningUp || !email || !password || !confirmPassword}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px 16px',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#059669',
|
||||
color: '#ffffff',
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
cursor: isSigningUp || !email || !password || !confirmPassword ? 'not-allowed' : 'pointer',
|
||||
opacity: isSigningUp || !email || !password || !confirmPassword ? 0.6 : 1,
|
||||
marginBottom: '20px'
|
||||
}}
|
||||
>
|
||||
{isSigningUp ? 'Creating Account...' : 'Create Account'}
|
||||
</button>
|
||||
|
||||
{/* Sign In Link */}
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<button
|
||||
onClick={() => navigate('/login')}
|
||||
disabled={isSigningUp}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#3b82f6',
|
||||
fontSize: '14px',
|
||||
cursor: isSigningUp ? 'not-allowed' : 'pointer',
|
||||
textDecoration: 'underline',
|
||||
opacity: isSigningUp ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
Already have an account? Sign in
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
marginTop: '24px',
|
||||
paddingTop: '20px',
|
||||
borderTop: '1px solid #e5e7eb'
|
||||
}}>
|
||||
<p style={{
|
||||
color: '#9ca3af',
|
||||
fontSize: '12px',
|
||||
margin: '0'
|
||||
}}>
|
||||
By creating an account, you agree to our terms of service
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user