This commit is contained in:
MartinBraquet
2025-07-27 16:45:52 +02:00
parent 7741de2a1c
commit 94e4691b0e
10 changed files with 378 additions and 583 deletions

View File

@@ -8,7 +8,7 @@ generator client {
datasource db {
provider = "sqlite"
url = "file:./dev.db"
url = env("DATABASE_URL")
}
model Account {
@@ -26,6 +26,10 @@ model Account {
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
displayName String
avatarUrl String?
bio String?
@@unique([provider, providerAccountId])
}

View File

@@ -4,9 +4,6 @@ import { authConfig } from '@/auth';
// Initialize NextAuth with the configuration
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
...authConfig,
// Enable debug logs in development
@@ -25,4 +22,4 @@ export const {
},
});
export { GET, POST, auth, signIn, signOut };
export { GET, POST};

View File

@@ -1,12 +1,12 @@
'use client';
import { signOut } from '@/app/api/auth/[...nextauth]/route';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import {signOut, useSession} from "next-auth/react";
import {useRouter} from 'next/navigation';
import {useEffect} from 'react';
export default function DashboardPage() {
const { data: session, status } = useSession();
console.log('DashboardPage');
const {data: session, status} = useSession();
const router = useRouter();
useEffect(() => {
@@ -16,6 +16,7 @@ export default function DashboardPage() {
}, [status, router]);
if (status === 'loading') {
console.log("LOADING");
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500"></div>
@@ -23,7 +24,9 @@ export default function DashboardPage() {
);
}
if (status === 'unauthenticated') {
console.log(status, session);
if (status === 'unauthenticated' || !session) {
// router.push('/login');
return null;
}
@@ -41,7 +44,7 @@ export default function DashboardPage() {
Welcome, {session.user?.name || 'User'}!
</span>
<button
onClick={() => signOut({ redirect: true, callbackUrl: '/' })}
onClick={() => signOut({redirect: true, callbackUrl: '/'})}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Sign out
@@ -56,15 +59,17 @@ export default function DashboardPage() {
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">Dashboard</h2>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
{/* User Info Card */}
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<div className="flex items-center">
<div className="flex-shrink-0 bg-indigo-500 rounded-md p-3">
<svg className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
<svg className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
<div className="ml-5 w-0 flex-1">
@@ -89,8 +94,10 @@ export default function DashboardPage() {
<div className="px-4 py-5 sm:p-6">
<div className="flex items-center">
<div className="flex-shrink-0 bg-green-500 rounded-md p-3">
<svg className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
<svg className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
</div>
<div className="ml-5 w-0 flex-1">
@@ -110,13 +117,17 @@ export default function DashboardPage() {
<div className="px-4 py-5 sm:p-6">
<div className="flex items-center">
<div className="flex-shrink-0 bg-yellow-500 rounded-md p-3">
<svg className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Last Login</dt>
<dt className="text-sm font-medium text-gray-500 truncate">Last
Login
</dt>
<dd className="flex items-baseline">
<div className="text-2xl font-semibold text-gray-900">
Just now
@@ -137,14 +148,18 @@ export default function DashboardPage() {
<li className="px-6 py-4">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center">
<svg className="h-6 w-6 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
<div
className="h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center">
<svg className="h-6 w-6 text-indigo-600" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round"
strokeWidth={2} d="M5 13l4 4L19 7"/>
</svg>
</div>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-900">Successfully logged in</p>
<p className="text-sm font-medium text-gray-900">Successfully logged
in</p>
<p className="text-sm text-gray-500">A few seconds ago</p>
</div>
</div>

View File

@@ -1,286 +1,282 @@
'use client';
import { signIn } from 'next-auth/react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useState, useEffect } from 'react';
import {signIn} from 'next-auth/react';
import {useRouter, useSearchParams} from 'next/navigation';
import {useState, useEffect} from 'react';
import Link from 'next/link';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get('callbackUrl') || '/dashboard';
const errorParam = searchParams.get('error');
const registered = searchParams.get('registered');
// Handle URL parameters and errors
useEffect(() => {
const newSearchParams = new URLSearchParams(searchParams);
// Handle error parameter
if (errorParam) {
let errorMessage = 'An error occurred during login';
// Map common error codes to user-friendly messages
if (errorParam === 'CredentialsSignin') {
errorMessage = 'Invalid email or password';
} else if (errorParam === 'OAuthAccountNotLinked') {
errorMessage = 'This email is already associated with another account';
} else if (errorParam === 'OAuthCallbackError') {
errorMessage = 'An error occurred during social sign in';
} else if (errorParam === 'AccessDenied') {
errorMessage = 'You do not have permission to sign in';
} else if (errorParam === 'Verification') {
errorMessage = 'Account not verified. Please check your email.';
}
setError(errorMessage);
// Clean up the URL
newSearchParams.delete('error');
router.replace(`/login?${newSearchParams.toString()}`);
}
// Show success message if user was just registered
if (registered) {
setSuccess('Registration successful! Please sign in with your credentials.');
newSearchParams.delete('registered');
router.replace(`/login?${newSearchParams.toString()}`);
}
}, [errorParam, registered, router, searchParams]);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get('callbackUrl') || '/dashboard';
const errorParam = searchParams.get('error');
const registered = searchParams.get('registered');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setSuccess('');
setIsLoading(true);
// Handle URL parameters and errors
useEffect(() => {
const newSearchParams = new URLSearchParams(searchParams);
try {
// Validate inputs
if (!email || !password) {
throw new Error('Email and password are required');
}
// Handle error parameter
if (errorParam) {
let errorMessage = 'An error occurred during login';
// Basic email validation
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
throw new Error('Please enter a valid email address');
}
// Map common error codes to user-friendly messages
if (errorParam === 'CredentialsSignin') {
errorMessage = 'Invalid email or password';
} else if (errorParam === 'OAuthAccountNotLinked') {
errorMessage = 'This email is already associated with another account';
} else if (errorParam === 'OAuthCallbackError') {
errorMessage = 'An error occurred during social sign in';
} else if (errorParam === 'AccessDenied') {
errorMessage = 'You do not have permission to sign in';
} else if (errorParam === 'Verification') {
errorMessage = 'Account not verified. Please check your email.';
}
// Attempt to sign in
const result = await signIn('credentials', {
redirect: false,
email: email.trim(),
password,
callbackUrl,
});
setError(errorMessage);
// Handle the result
if (result?.error) {
// Handle specific error messages
let errorMessage = 'Invalid email or password';
if (result.error === 'CredentialsSignin') {
errorMessage = 'Invalid email or password';
} else if (result.error === 'AccessDenied') {
errorMessage = 'You do not have permission to sign in';
} else if (result.error === 'Configuration') {
errorMessage = 'Server configuration error';
} else if (result.error === 'Verification') {
errorMessage = 'Account not verified. Please check your email.';
} else if (result.error === 'OAuthSignin') {
errorMessage = 'Error in OAuth sign in. Please try again.';
} else if (result.error === 'OAuthCallback') {
errorMessage = 'Error in OAuth callback. Please try again.';
} else if (result.error === 'OAuthCreateAccount') {
errorMessage = 'Error creating OAuth account. Please try again.';
} else if (result.error === 'EmailCreateAccount') {
errorMessage = 'Error creating account. Please try again.';
} else if (result.error === 'Callback') {
errorMessage = 'Error in authentication callback. Please try again.';
} else if (result.error === 'OAuthAccountNotLinked') {
errorMessage = 'This email is already associated with another account';
// Clean up the URL
newSearchParams.delete('error');
router.replace(`/login?${newSearchParams.toString()}`);
}
throw new Error(errorMessage);
}
// If we got here, sign in was successful
if (result?.url) {
// Ensure we're using the correct callback URL
const url = new URL(result.url);
const callbackUrlParam = url.searchParams.get('callbackUrl');
const finalUrl = callbackUrlParam || callbackUrl;
// Force a full page reload to ensure all session data is loaded
window.location.href = finalUrl;
} else {
// Fallback in case result.url is not available
window.location.href = callbackUrl;
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
setIsLoading(false);
}
};
// Show success message if user was just registered
if (registered) {
setSuccess('Registration successful! Please sign in with your credentials.');
newSearchParams.delete('registered');
router.replace(`/login?${newSearchParams.toString()}`);
}
}, [errorParam, registered, router, searchParams]);
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in to your account
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Don't have an account?{' '}
<Link
href={`/signup${callbackUrl ? `?callbackUrl=${encodeURIComponent(callbackUrl)}` : ''}`}
className="font-medium text-indigo-600 hover:text-indigo-500"
>
Sign up
</Link>
</p>
<div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">Or continue with</span>
</div>
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setSuccess('');
setIsLoading(true);
try {
// Validate inputs
if (!email || !password) {
throw new Error('Email and password are required');
}
// Basic email validation
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
throw new Error('Please enter a valid email address');
}
// Attempt to sign in
const result = await signIn('credentials', {
redirect: false,
email: email.trim(),
password,
callbackUrl,
});
// Handle the result
if (result?.error) {
// Handle specific error messages
let errorMessage = 'Invalid email or password';
if (result.error === 'CredentialsSignin') {
errorMessage = 'Invalid email or password';
} else if (result.error === 'AccessDenied') {
errorMessage = 'You do not have permission to sign in';
} else if (result.error === 'Configuration') {
errorMessage = 'Server configuration error';
} else if (result.error === 'Verification') {
errorMessage = 'Account not verified. Please check your email.';
} else if (result.error === 'OAuthSignin') {
errorMessage = 'Error in OAuth sign in. Please try again.';
} else if (result.error === 'OAuthCallback') {
errorMessage = 'Error in OAuth callback. Please try again.';
} else if (result.error === 'OAuthCreateAccount') {
errorMessage = 'Error creating OAuth account. Please try again.';
} else if (result.error === 'EmailCreateAccount') {
errorMessage = 'Error creating account. Please try again.';
} else if (result.error === 'Callback') {
errorMessage = 'Error in authentication callback. Please try again.';
} else if (result.error === 'OAuthAccountNotLinked') {
errorMessage = 'This email is already associated with another account';
}
throw new Error(errorMessage);
}
// If we got here, sign in was successful
if (result?.url) {
// Ensure we're using the correct callback URL
const url = new URL(result.url);
const callbackUrlParam = url.searchParams.get('callbackUrl');
const finalUrl = callbackUrlParam || callbackUrl;
// Force a full page reload to ensure all session data is loaded
window.location.href = finalUrl;
} else {
// Fallback in case result.url is not available
window.location.href = callbackUrl;
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
setIsLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in to your account
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Don't have an account?{' '}
<Link
href={`/signup${callbackUrl ? `?callbackUrl=${encodeURIComponent(callbackUrl)}` : ''}`}
className="font-medium text-indigo-600 hover:text-indigo-500"
>
Sign up
</Link>
</p>
<div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">Or continue with</span>
</div>
</div>
<div className="mt-6 flex justify-center">
<div className="w-full max-w-xs">
<div>
<button
onClick={() => signIn('google', {callbackUrl})}
className="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
>
<span className="sr-only">Sign in with Google</span>
<svg className="w-5 h-5" aria-hidden="true" fill="currentColor"
viewBox="0 0 24 24">
<path
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
{success && (
<div className="bg-green-50 border-l-4 border-green-500 p-4 mb-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-green-500" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"/>
</svg>
</div>
<div className="ml-3">
<p className="text-sm text-green-700">{success}</p>
</div>
</div>
</div>
)}
{error && (
<div className="bg-red-50 border-l-4 border-red-500 p-4 mb-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"/>
</svg>
</div>
<div className="ml-3">
<p className="text-sm text-red-700">{error}</p>
</div>
</div>
</div>
)}
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<input type="hidden" name="remember" value="true"/>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email-address" className="sr-only">
Email address
</label>
<input
id="email-address"
name="email"
type="email"
autoComplete="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
/>
<label
htmlFor="remember-me"
className="ml-2 block text-sm text-gray-900"
>
Remember me
</label>
</div>
<div className="text-sm">
<a
href="#"
className="font-medium text-indigo-600 hover:text-indigo-500"
>
Forgot your password?
</a>
</div>
</div>
<div>
<button
type="submit"
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Sign in
</button>
</div>
</form>
</div>
<div className="mt-6 grid grid-cols-2 gap-3">
<div>
<button
onClick={() => signIn('google', { callbackUrl })}
className="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
>
<span className="sr-only">Sign in with Google</span>
<svg className="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 24 24">
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
</svg>
</button>
</div>
<div>
<button
onClick={() => signIn('github', { callbackUrl })}
className="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
>
<span className="sr-only">Sign in with GitHub</span>
<svg className="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.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.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.699 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C17.14 18.25 20 14.42 20 10.017 20 4.484 15.522 0 10 0z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
</div>
{success && (
<div className="bg-green-50 border-l-4 border-green-500 p-4 mb-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-green-500" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<p className="text-sm text-green-700">{success}</p>
</div>
</div>
</div>
)}
{error && (
<div className="bg-red-50 border-l-4 border-red-500 p-4 mb-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<p className="text-sm text-red-700">{error}</p>
</div>
</div>
</div>
)}
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<input type="hidden" name="remember" value="true" />
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email-address" className="sr-only">
Email address
</label>
<input
id="email-address"
name="email"
type="email"
autoComplete="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
/>
<label
htmlFor="remember-me"
className="ml-2 block text-sm text-gray-900"
>
Remember me
</label>
</div>
<div className="text-sm">
<a
href="#"
className="font-medium text-indigo-600 hover:text-indigo-500"
>
Forgot your password?
</a>
</div>
</div>
<div>
<button
type="submit"
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Sign in
</button>
</div>
</form>
</div>
</div>
);
);
}

View File

@@ -6,17 +6,17 @@ export default function Home() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center">
<h1 className="text-4xl font-extrabold text-gray-900 sm:text-5xl sm:tracking-tight lg:text-6xl">
Welcome to BayesBond
BayesBond
</h1>
<p className="mt-3 max-w-md mx-auto text-base text-gray-500 sm:text-lg md:mt-5 md:text-xl md:max-w-3xl">
A modern web application with authentication and more.
A bonding platform for rational thinkers
</p>
<div className="mt-10 flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/signup"
className="px-8 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 md:py-4 md:text-lg md:px-10 transition-colors"
>
Get Started
Sign Up
</Link>
<Link
href="/login"
@@ -26,41 +26,6 @@ export default function Home() {
</Link>
</div>
</div>
<div className="mt-16">
<h2 className="text-2xl font-bold text-gray-900 text-center mb-8">Features</h2>
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
<div className="bg-white p-6 rounded-lg shadow">
<div className="text-indigo-600 mb-4">
<svg className="h-8 w-8" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clipRule="evenodd" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Fast</h3>
<p className="text-gray-600">Built with Next.js for optimal performance and SEO.</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="text-indigo-600 mb-4">
<svg className="h-8 w-8" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Secure</h3>
<p className="text-gray-600">Built-in authentication with NextAuth.js.</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="text-indigo-600 mb-4">
<svg className="h-8 w-8" fill="currentColor" viewBox="0 0 20 20">
<path d="M11 17a1 1 0 001.447.894l4-2A1 1 0 0017 15V9.236a1 1 0 00-1.447-.894l-4 2a1 1 0 00-.553.894V17zM15.211 6.276a1 1 0 000-1.788l-4.764-2.382a1 1 0 00-.894 0L4.789 4.488a1 1 0 000 1.788l4.764 2.382a1 1 0 00.894 0l4.764-2.382zM4.447 8.342A1 1 0 003 9.236V15a1 1 0 00.553.894l4 2A1 1 0 009 17v-5.764a1 1 0 00-.553-.894l-4-2z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Modern</h3>
<p className="text-gray-600">Built with the latest web technologies.</p>
</div>
</div>
</div>
</div>
</div>
);

View File

@@ -1,103 +0,0 @@
import Image from "next/image";
export default function Home() {
return (
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
}

View File

@@ -110,6 +110,70 @@ export default function SignupPage() {
Sign in
</Link>
</p>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="name" className="sr-only">
Full Name
</label>
<input
id="name"
name="name"
type="text"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Full Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label htmlFor="email-address" className="sr-only">
Email address
</label>
<input
id="email-address"
name="email"
type="email"
autoComplete="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="new-password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
<div>
<button
type="submit"
disabled={isLoading}
className={`group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 ${
isLoading ? 'opacity-70 cursor-not-allowed' : ''
}`}
>
{isLoading ? 'Creating account...' : 'Sign up'}
</button>
</div>
</form>
<div className="mt-6">
<div className="relative">
@@ -121,8 +185,8 @@ export default function SignupPage() {
</div>
</div>
<div className="mt-6 grid grid-cols-2 gap-3">
<div>
<div className="mt-6 flex justify-center">
<div className="w-full max-w-xs">
<button
onClick={() => signIn('google', { callbackUrl })}
className="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
@@ -133,18 +197,6 @@ export default function SignupPage() {
</svg>
</button>
</div>
<div>
<button
onClick={() => signIn('github', { callbackUrl })}
className="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
>
<span className="sr-only">Sign in with GitHub</span>
<svg className="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.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.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.699 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C17.14 18.25 20 14.42 20 10.017 20 4.484 15.522 0 10 0z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
</div>
@@ -171,69 +223,7 @@ export default function SignupPage() {
</div>
</div>
)}
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="name" className="sr-only">
Full Name
</label>
<input
id="name"
name="name"
type="text"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Full Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label htmlFor="email-address" className="sr-only">
Email address
</label>
<input
id="email-address"
name="email"
type="email"
autoComplete="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="new-password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
<div>
<button
type="submit"
disabled={isLoading}
className={`group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 ${
isLoading ? 'opacity-70 cursor-not-allowed' : ''
}`}
>
{isLoading ? 'Creating account...' : 'Sign up'}
</button>
</div>
</form>
</div>
</div>
);

View File

@@ -1,70 +0,0 @@
import { PrismaAdapter } from "@auth/prisma-adapter";
import { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { prisma } from "./prisma";
import { compare } from 'bcryptjs';
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
session: {
strategy: "jwt",
},
pages: {
signIn: "/login",
signOut: "/",
error: "/login",
},
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
throw new Error("Please enter your email and password");
}
const user = await prisma.user.findUnique({
where: {
email: credentials.email,
},
});
if (!user || !user.password) {
throw new Error("No user found with this email");
}
const isValid = await compare(credentials.password, user.password);
if (!isValid) {
throw new Error("Incorrect password");
}
return {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
};
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
},
async session({ session, token }) {
if (session?.user) {
session.user.id = token.id as string;
}
return session;
},
},
secret: process.env.NEXTAUTH_SECRET,
debug: process.env.NODE_ENV === "development",
} as const;

View File

@@ -5,7 +5,7 @@ declare global {
var prisma: PrismaClient | undefined;
}
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
const globalForPrisma = global as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma || new PrismaClient();

View File

@@ -23,5 +23,6 @@
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"exclude": ["node_modules"],
"sourceMap": true
}