Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions EnvExample.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
EMAIL_USER=Your App Email
EMAIL_PASSWORD=Your Google App Password
NEXT_PUBLIC_GA_ID=Your Google Analytics ID

NEXT_PUBLIC_SUPABASE_URL=Your supabase Url
NEXT_PUBLIC_SUPABASE_ANON_KEY=Your Anon Key
NEXT_PUBLIC_TURNSTILE_SITE_KEY=Your Cloudfare Captcha Key

TURNSTILE_SECRET_KEY=Your Cloudfare backend route api key
SUPABASE_SERVICE_KEY=Your supabase service key
52 changes: 52 additions & 0 deletions app/api/auth/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY
)

export async function POST(req) {
try {
const { email, password, captchaToken, action } = await req.json()

if (!email || !password)
return new Response(JSON.stringify({ message: 'Email and password required' }), { status: 400 })

if (action === 'signup') {
if (!captchaToken)
return new Response(JSON.stringify({ message: 'Captcha token missing' }), { status: 400 })

// Verify Turnstile token
const verifyRes = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
secret: process.env.TURNSTILE_SECRET_KEY,
response: captchaToken,
}),
})

const data = await verifyRes.json()
if (!data.success)
return new Response(JSON.stringify({ message: 'Captcha verification failed' }), { status: 400 })

// Create Supabase user
const { user, error } = await supabase.auth.admin.createUser({ email, password })
if (error)
return new Response(JSON.stringify({ message: error.message }), { status: 400 })

return new Response(JSON.stringify({ message: 'Signup successful! Check your email.' }), { status: 200 })
}

else if (action === 'login') {
return new Response(JSON.stringify({ message: 'Use frontend login with anon key' }), { status: 400 })
}

else {
return new Response(JSON.stringify({ message: 'Invalid action' }), { status: 400 })
}
} catch (err) {
console.error(err)
return new Response(JSON.stringify({ message: 'Internal server error' }), { status: 500 })
}
}
106 changes: 45 additions & 61 deletions app/login/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { useRouter } from 'next/navigation'
import { FiMail, FiLock, FiUser, FiLogIn, FiUserPlus, FiSun, FiMoon } from 'react-icons/fi'
import { motion } from 'framer-motion'
import Link from 'next/link'
import dynamic from "next/dynamic";
import dynamic from 'next/dynamic'

const Turnstile = dynamic(
() => import("@marsidev/react-turnstile").then((mod) => mod.Turnstile),
() => import('@marsidev/react-turnstile').then((mod) => mod.Turnstile),
{ ssr: false }
);
)

export default function LoginPage() {
const [email, setEmail] = useState('')
Expand Down Expand Up @@ -38,17 +38,28 @@ export default function LoginPage() {
const handleAuth = async () => {
setLoading(true)
setError('')

try {
if (isLogin) {
// Login with frontend anon key
const { error } = await supabase.auth.signInWithPassword({ email, password })
if (error) throw error
router.push('/dashboard')
} else {
// Signup via API route with Turnstile
if (!captchaToken) throw new Error('Please complete captcha')
const { error } = await supabase.auth.signUp({ email, password, options: { captchaToken } })
if (error) throw error
alert('Check your email for confirmation!')

const res = await fetch('/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, captchaToken, action: 'signup' }),
})

const data = await res.json()
if (!res.ok) throw new Error(data.message)

alert(data.message)
setIsLogin(true) // switch to login after signup
}
} catch (err) {
setError(err.message)
Expand All @@ -57,60 +68,47 @@ export default function LoginPage() {
}
}

const handleGoogleSignIn = async () => {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
})
if (error) console.error('Error signing in with Google:', error.message)
const handleGoogleSignIn = async () => {
const { error } = await supabase.auth.signInWithOAuth({ provider: 'google' })
if (error) console.error('Google sign-in error:', error.message)
}

return (
<div className="min-h-screen dark:bg-gray-950 bg-gray-50 flex items-center justify-center p-4">
<motion.div
<div className="min-h-screen flex items-center justify-center p-4 bg-gray-50 dark:bg-gray-950">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="w-full max-w-md bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden border border-gray-200 dark:border-gray-700"
>
{/* Header with theme toggle */}
{/* Header */}
<div className="bg-blue-600 p-6 text-white flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold">{isLogin ? 'Welcome Back' : 'Create Account'}</h1>
<p className="text-blue-100 dark:text-blue-200">
{isLogin ? 'Sign in to access your dashboard' : 'Join us to get started'}
</p>
</div>
<button
onClick={toggleTheme}
className="p-2 rounded-full hover:bg-blue-700 transition duration-200"
aria-label="Toggle theme"
>
{theme === 'light' ? (
<FiMoon className="w-5 h-5 text-white" />
) : (
<FiSun className="w-5 h-5 text-white" />
)}
<button onClick={toggleTheme} className="p-2 rounded-full hover:bg-blue-700" aria-label="Toggle theme">
{theme === 'light' ? <FiMoon className="w-5 h-5" /> : <FiSun className="w-5 h-5" />}
</button>
</div>

<div className="p-6 space-y-4">
{error && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="bg-red-100 dark:bg-red-900/30 border-l-4 border-red-500 text-red-700 dark:text-red-300 p-3 rounded"
>
<p>{error}</p>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="bg-red-100 dark:bg-red-900/30 border-l-4 border-red-500 text-red-700 dark:text-red-300 p-3 rounded">
{error}
</motion.div>
)}

{/* Form */}
<div className="space-y-4">
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<FiMail className="text-gray-400 dark:text-gray-500" />
</div>
<input
type="email"
className="w-full pl-10 pr-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200"
className="w-full pl-10 pr-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-200"
placeholder="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
Expand All @@ -123,7 +121,7 @@ export default function LoginPage() {
</div>
<input
type="password"
className="w-full pl-10 pr-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200"
className="w-full pl-10 pr-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-200"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
Expand All @@ -137,7 +135,7 @@ export default function LoginPage() {
</div>
<input
type="text"
className="w-full pl-10 pr-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200"
className="w-full pl-10 pr-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-200"
placeholder="Full name (optional)"
/>
</div>
Expand All @@ -156,69 +154,55 @@ export default function LoginPage() {
onClick={handleAuth}
disabled={loading}
className={`w-full flex items-center justify-center py-3 px-4 rounded-lg text-white font-medium transition-all ${
loading
? 'bg-gray-400 dark:bg-gray-600 cursor-not-allowed'
: 'bg-blue-600 hover:from-blue-700 hover:to-purple-700 dark:hover:from-blue-800 dark:hover:to-purple-800 shadow-md hover:shadow-lg'
loading
? 'bg-gray-400 dark:bg-gray-600 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-800 shadow-md hover:shadow-lg'
}`}
>
{loading ? (
'Processing...'
) : isLogin ? (
<>
<FiLogIn className="mr-2" /> Sign In
</>
) : (
<>
{isLogin ? (
<>
<FiLogIn className="mr-2" /> Sign In
</>
) : (
<>
<FiUserPlus className="mr-2" /> Sign Up
</>
)}
<FiUserPlus className="mr-2" /> Sign Up
</>
)}
</button>
</div>

{/* Switch forms */}
<div className="text-center text-sm text-gray-600 dark:text-gray-400">
{isLogin ? (
<p>
Don't have an account?{' '}
<button
onClick={() => setIsLogin(false)}
className="text-blue-600 dark:text-blue-400 hover:underline font-medium"
>
<button onClick={() => setIsLogin(false)} className="text-blue-600 dark:text-blue-400 hover:underline">
Sign up
</button>
</p>
) : (
<p>
Already have an account?{' '}
<button
onClick={() => setIsLogin(true)}
className="text-blue-600 dark:text-blue-400 hover:underline font-medium"
>
<button onClick={() => setIsLogin(true)} className="text-blue-600 dark:text-blue-400 hover:underline">
Sign in
</button>
</p>
)}
</div>

{/* Google OAuth */}
<div className="relative flex items-center py-4">
<div className="flex-grow border-t border-gray-300 dark:border-gray-600"></div>
<span className="flex-shrink mx-4 text-gray-500 dark:text-gray-400">or</span>
<div className="flex-grow border-t border-gray-300 dark:border-gray-600"></div>
</div>

<button
onClick={() => supabase.auth.signInWithOAuth({ provider: 'google' })}
onClick={handleGoogleSignIn}
className="w-full flex items-center justify-center py-3 px-4 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium hover:bg-gray-50 dark:hover:bg-gray-600 transition-all"
>
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
</svg>
Continue with Google
</button>

Expand Down
Loading