Initial commit
This commit is contained in:
211
frontend/src/pages/auth/Login.jsx
Normal file
211
frontend/src/pages/auth/Login.jsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
import { BoltIcon, EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
export default function Login() {
|
||||
const navigate = useNavigate()
|
||||
const { login, nostrLogin } = useAuthStore()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isNostrLoading, setIsNostrLoading] = useState(false)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
await login(formData.email, formData.password)
|
||||
toast.success('Welcome back!')
|
||||
navigate('/dashboard')
|
||||
} catch (error) {
|
||||
toast.error(error.response?.data?.error || 'Failed to log in')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNostrLogin = async () => {
|
||||
if (!window.nostr) {
|
||||
toast.error('No Nostr extension found. Please install Alby, nos2x, or another Nostr signer.')
|
||||
return
|
||||
}
|
||||
|
||||
setIsNostrLoading(true)
|
||||
|
||||
try {
|
||||
// Get public key from extension
|
||||
const pubkey = await window.nostr.getPublicKey()
|
||||
|
||||
// Create a login event (kind 27235 is NIP-98 HTTP Auth)
|
||||
const event = {
|
||||
kind: 27235,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['u', window.location.origin + '/api/auth/nostr/verify'],
|
||||
['method', 'POST'],
|
||||
],
|
||||
content: 'Login to LNPaywall',
|
||||
pubkey,
|
||||
}
|
||||
|
||||
// Sign the event
|
||||
const signedEvent = await window.nostr.signEvent(event)
|
||||
|
||||
// Send to backend
|
||||
await nostrLogin(pubkey, signedEvent)
|
||||
toast.success('Welcome back!')
|
||||
navigate('/dashboard')
|
||||
} catch (error) {
|
||||
console.error('Nostr login error:', error)
|
||||
toast.error(error.response?.data?.error || error.message || 'Failed to login with Nostr')
|
||||
} finally {
|
||||
setIsNostrLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center px-4 py-16 bg-dark-950">
|
||||
{/* Background effects */}
|
||||
<div className="absolute inset-0 bg-grid opacity-20" />
|
||||
<div className="absolute inset-0 bg-gradient-radial from-lightning/5 via-transparent to-transparent" />
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="relative w-full max-w-md"
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<Link to="/" className="inline-flex items-center gap-2 mb-4">
|
||||
<span className="text-3xl">⚡</span>
|
||||
<span className="font-display font-bold text-2xl">LNPaywall</span>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold">Welcome back</h1>
|
||||
<p className="text-dark-400 mt-2">Log in to your account</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="card p-8">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="email" className="label">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className="input"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="label">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
required
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
className="input pr-12"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-dark-400 hover:text-white"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeSlashIcon className="w-5 h-5" />
|
||||
) : (
|
||||
<EyeIcon className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="btn btn-primary w-full"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<BoltIcon className="w-5 h-5" />
|
||||
Log in
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="relative my-8">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-dark-700" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-4 bg-dark-900 text-dark-400">or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Social logins */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={handleNostrLogin}
|
||||
disabled={isNostrLoading}
|
||||
>
|
||||
{isNostrLoading ? (
|
||||
<div className="w-5 h-5 border-2 border-purple-500/30 border-t-purple-500 rounded-full animate-spin" />
|
||||
) : (
|
||||
<span className="text-xl">🟣</span>
|
||||
)}
|
||||
Nostr
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => toast('GitHub login coming soon!')}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
|
||||
</svg>
|
||||
GitHub
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sign up link */}
|
||||
<p className="text-center mt-6 text-dark-400">
|
||||
Don't have an account?{' '}
|
||||
<Link to="/signup" className="text-lightning hover:underline">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user