mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-03-02 13:58:18 -05:00
Update Auth
This commit is contained in:
47
middleware.ts
Normal file
47
middleware.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { getToken } from 'next-auth/jwt';
|
||||
|
||||
const publicPaths = ['/', '/login', '/signup', '/api/auth/signin', '/api/auth/signout'];
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
const { pathname } = req.nextUrl;
|
||||
const token = await getToken({ req });
|
||||
|
||||
// Allow access to public paths and static files
|
||||
if (
|
||||
publicPaths.some(path => path === pathname) ||
|
||||
pathname.startsWith('/_next') ||
|
||||
pathname.startsWith('/favicon.ico') ||
|
||||
pathname.startsWith('/public/')
|
||||
) {
|
||||
// If user is logged in and tries to access auth pages, redirect to dashboard
|
||||
if (token && (pathname.startsWith('/login') || pathname.startsWith('/signup'))) {
|
||||
return NextResponse.redirect(new URL('/dashboard', req.url));
|
||||
}
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// If no token and not a public path, redirect to login
|
||||
if (!token) {
|
||||
const loginUrl = new URL('/login', req.url);
|
||||
loginUrl.searchParams.set('callbackUrl', pathname);
|
||||
return NextResponse.redirect(loginUrl);
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except for the ones starting with:
|
||||
* - api (API routes)
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
* - public folder
|
||||
*/
|
||||
'/((?!api|_next/static|_next/image|favicon.ico|public).*)',
|
||||
],
|
||||
};
|
||||
57
prisma/migrations/20250726215047_init/migration.sql
Normal file
57
prisma/migrations/20250726215047_init/migration.sql
Normal file
@@ -0,0 +1,57 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Account" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"providerAccountId" TEXT NOT NULL,
|
||||
"refresh_token" TEXT,
|
||||
"access_token" TEXT,
|
||||
"expires_at" INTEGER,
|
||||
"token_type" TEXT,
|
||||
"scope" TEXT,
|
||||
"id_token" TEXT,
|
||||
"session_state" TEXT,
|
||||
CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT,
|
||||
"email" TEXT,
|
||||
"email_verified" DATETIME,
|
||||
"image" TEXT,
|
||||
"password" TEXT
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"sessionToken" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"expires" DATETIME NOT NULL,
|
||||
CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "VerificationToken" (
|
||||
"identifier" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"expires" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "sqlite"
|
||||
@@ -7,8 +7,8 @@ generator client {
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
provider = "sqlite"
|
||||
url = "file:./dev.db"
|
||||
}
|
||||
|
||||
model Account {
|
||||
@@ -17,12 +17,12 @@ model Account {
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? @db.Text
|
||||
access_token String? @db.Text
|
||||
refresh_token String?
|
||||
access_token String?
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? @db.Text
|
||||
id_token String?
|
||||
session_state String?
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
import NextAuth from "next-auth";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import NextAuth from 'next-auth';
|
||||
import { authConfig } from '@/auth';
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
// Initialize NextAuth with the configuration
|
||||
export const {
|
||||
handlers: { GET, POST },
|
||||
auth,
|
||||
signIn,
|
||||
signOut,
|
||||
} = NextAuth({
|
||||
...authConfig,
|
||||
// Enable debug logs in development
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
// Ensure cookies are secure in production
|
||||
cookies: {
|
||||
sessionToken: {
|
||||
name: `__Secure-next-auth.session-token`,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax', // CSRF protection
|
||||
path: '/',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
export { GET, POST, auth, signIn, signOut };
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { signOut, useSession } from 'next-auth/react';
|
||||
import { signOut } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
@@ -22,8 +23,9 @@ export default function DashboardPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return null; // Will be redirected by the useEffect
|
||||
if (status === 'unauthenticated') {
|
||||
router.push('/login');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -34,9 +36,12 @@ export default function DashboardPage() {
|
||||
<div className="flex items-center">
|
||||
<h1 className="text-xl font-bold text-gray-900">Dashboard</h1>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-sm text-gray-700">
|
||||
Welcome, {session.user?.name || 'User'}!
|
||||
</span>
|
||||
<button
|
||||
onClick={() => signOut({ 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
|
||||
@@ -48,10 +53,106 @@ export default function DashboardPage() {
|
||||
|
||||
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<div className="px-4 py-6 sm:px-0">
|
||||
<div className="border-4 border-dashed border-gray-200 rounded-lg h-96 p-6 text-center">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Welcome, {session.user?.name || 'User'}!</h2>
|
||||
<p className="text-gray-600">You are now logged in.</p>
|
||||
<p className="text-gray-600 mt-2">Email: {session.user?.email}</p>
|
||||
<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>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Account</dt>
|
||||
<dd className="flex items-baseline">
|
||||
<div className="text-2xl font-semibold text-gray-900">
|
||||
{session.user?.name || 'User'}
|
||||
</div>
|
||||
</dd>
|
||||
<dd className="text-sm text-gray-500 truncate">
|
||||
{session.user?.email}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Card 1 */}
|
||||
<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-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>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Projects</dt>
|
||||
<dd className="flex items-baseline">
|
||||
<div className="text-2xl font-semibold text-gray-900">3</div>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Card 2 */}
|
||||
<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-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>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<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
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="mt-8">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Recent Activity</h3>
|
||||
<div className="bg-white shadow overflow-hidden sm:rounded-md">
|
||||
<ul className="divide-y divide-gray-200">
|
||||
<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" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,34 +1,128 @@
|
||||
'use client';
|
||||
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from '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 handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setSuccess('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const result = await signIn('credentials', {
|
||||
redirect: false,
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
setError(result.error);
|
||||
return;
|
||||
// Validate inputs
|
||||
if (!email || !password) {
|
||||
throw new Error('Email and password are required');
|
||||
}
|
||||
|
||||
router.push('/dashboard');
|
||||
// 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('An error occurred. Please try again.');
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -40,30 +134,72 @@ export default function LoginPage() {
|
||||
Sign in to your account
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Or{' '}
|
||||
<a
|
||||
href="/signup"
|
||||
Don't have an account?{' '}
|
||||
<Link
|
||||
href={`/signup${callbackUrl ? `?callbackUrl=${encodeURIComponent(callbackUrl)}` : ''}`}
|
||||
className="font-medium text-indigo-600 hover:text-indigo-500"
|
||||
>
|
||||
create a new account
|
||||
</a>
|
||||
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 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>
|
||||
{error && (
|
||||
<div className="bg-red-50 border-l-4 border-red-500 p-4">
|
||||
{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-red-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
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 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">
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { signIn } from 'next-auth/react';
|
||||
|
||||
export default function SignupPage() {
|
||||
const [name, setName] = useState('');
|
||||
@@ -11,28 +13,81 @@ export default function SignupPage() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const callbackUrl = searchParams.get('callbackUrl') || '/dashboard';
|
||||
|
||||
// Handle any error from the URL (e.g., from auth callback)
|
||||
useEffect(() => {
|
||||
const errorParam = searchParams.get('error');
|
||||
if (errorParam) {
|
||||
// Remove the error from the URL
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
newSearchParams.delete('error');
|
||||
router.replace(`/signup?${newSearchParams.toString()}`);
|
||||
|
||||
// Set appropriate error message
|
||||
let errorMessage = 'An error occurred during signup';
|
||||
if (errorParam === 'OAuthAccountNotLinked') {
|
||||
errorMessage = 'This email is already associated with another account';
|
||||
} else if (errorParam === 'OAuthCallbackError') {
|
||||
errorMessage = 'An error occurred during social sign in';
|
||||
}
|
||||
setError(errorMessage);
|
||||
}
|
||||
}, [router, searchParams]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Basic validation
|
||||
if (!name || !email || !password) {
|
||||
throw new Error('All fields are required');
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
throw new Error('Password must be at least 8 characters long');
|
||||
}
|
||||
|
||||
// Create user in the database
|
||||
const response = await fetch('/api/auth/signup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ name, email, password }),
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
email,
|
||||
password // The password will be hashed in the API route
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Something went wrong');
|
||||
throw new Error(data.message || 'Something went wrong during signup');
|
||||
}
|
||||
|
||||
// Redirect to login page after successful signup
|
||||
router.push('/login');
|
||||
// Automatically sign in the user after successful signup
|
||||
const signInResult = await signIn('credentials', {
|
||||
redirect: false,
|
||||
email,
|
||||
password,
|
||||
callbackUrl,
|
||||
});
|
||||
|
||||
if (signInResult?.error) {
|
||||
// If sign in fails, redirect to login page with a success message
|
||||
router.push(`/login?registered=true&callbackUrl=${encodeURIComponent(callbackUrl)}`);
|
||||
} else if (signInResult?.url) {
|
||||
// If sign in is successful, redirect to the callback URL or dashboard
|
||||
router.push(signInResult.url);
|
||||
} else {
|
||||
// Fallback redirect
|
||||
router.push(callbackUrl);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
setIsLoading(false);
|
||||
@@ -47,14 +102,51 @@ export default function SignupPage() {
|
||||
Create a new account
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Or{' '}
|
||||
<a
|
||||
href="/login"
|
||||
Already have an account?{' '}
|
||||
<Link
|
||||
href={`/login${callbackUrl ? `?callbackUrl=${encodeURIComponent(callbackUrl)}` : ''}`}
|
||||
className="font-medium text-indigo-600 hover:text-indigo-500"
|
||||
>
|
||||
sign in to your existing account
|
||||
</a>
|
||||
Sign in
|
||||
</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 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>
|
||||
{error && (
|
||||
<div className="bg-red-50 border-l-4 border-red-500 p-4">
|
||||
|
||||
67
src/auth.config.ts
Normal file
67
src/auth.config.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { NextAuthConfig } from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import { prisma } from "./lib/prisma";
|
||||
import { compare } from "bcryptjs";
|
||||
|
||||
export default {
|
||||
providers: [
|
||||
Credentials({
|
||||
name: "Credentials",
|
||||
credentials: {
|
||||
email: { label: "Email", type: "email" },
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
throw new Error("Email and password are required");
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: credentials.email as string },
|
||||
});
|
||||
|
||||
if (!user || !user.password) {
|
||||
throw new Error("Invalid email or password");
|
||||
}
|
||||
|
||||
const isValid = await compare(
|
||||
credentials.password as string,
|
||||
user.password
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error("Invalid email or password");
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
image: user.image,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async session({ session, token }) {
|
||||
if (token && session.user) {
|
||||
session.user.id = token.sub!;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.sub = user.id;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
},
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
error: "/login",
|
||||
},
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
} satisfies NextAuthConfig;
|
||||
83
src/auth.ts
Normal file
83
src/auth.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { NextAuthConfig } from 'next-auth';
|
||||
import Credentials from 'next-auth/providers/credentials';
|
||||
import { prisma } from './lib/prisma';
|
||||
import { compare } from 'bcryptjs';
|
||||
|
||||
export const authConfig = {
|
||||
pages: {
|
||||
signIn: '/login',
|
||||
error: '/login',
|
||||
},
|
||||
callbacks: {
|
||||
authorized({ auth, request: { nextUrl } }) {
|
||||
const isLoggedIn = !!auth?.user;
|
||||
const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
|
||||
|
||||
if (isOnDashboard) {
|
||||
return isLoggedIn;
|
||||
} else if (isLoggedIn) {
|
||||
return Response.redirect(new URL('/dashboard', nextUrl));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
token.email = user.email;
|
||||
token.name = user.name;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
session.user.id = token.id as string;
|
||||
session.user.name = token.name;
|
||||
session.user.email = token.email as string;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
providers: [
|
||||
Credentials({
|
||||
name: 'Credentials',
|
||||
credentials: {
|
||||
email: { label: 'Email', type: 'email' },
|
||||
password: { label: 'Password', type: 'password' },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
throw new Error('Email and password are required');
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: credentials.email as string },
|
||||
});
|
||||
|
||||
if (!user || !user.password) {
|
||||
throw new Error('Invalid email or password');
|
||||
}
|
||||
|
||||
const isValid = await compare(
|
||||
credentials.password as string,
|
||||
user.password
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid email or password');
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
image: user.image,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
session: {
|
||||
strategy: 'jwt',
|
||||
},
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
trustHost: true,
|
||||
} satisfies NextAuthConfig;
|
||||
@@ -1,14 +1,16 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { PrismaClient } from '@/generated/prisma';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var prisma: PrismaClient | undefined;
|
||||
}
|
||||
|
||||
export const prisma = global.prisma || new PrismaClient();
|
||||
const globalForPrisma = global as unknown as { prisma: PrismaClient };
|
||||
|
||||
export const prisma = globalForPrisma.prisma || new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
global.prisma = prisma;
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
|
||||
async function connectDB() {
|
||||
|
||||
Reference in New Issue
Block a user