Files
LNpaywall/frontend/src/pages/auth/Login.jsx
2025-12-14 23:08:45 -03:00

212 lines
7.4 KiB
JavaScript

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>
)
}