diff --git a/app/lib/actions.ts b/app/lib/actions.ts index 9a82aee..e484118 100644 --- a/app/lib/actions.ts +++ b/app/lib/actions.ts @@ -4,6 +4,9 @@ import { z } from 'zod'; import { sql } from '@vercel/postgres'; import { revalidatePath } from 'next/cache'; import {redirect} from "next/navigation"; +import { signIn } from '@/auth'; +import { AuthError } from 'next-auth'; + const FormSchema = z.object({ id: z.string(), @@ -97,4 +100,23 @@ export async function deleteInvoice(id: string) { } catch (error) { return { message: 'Database Error: Failed to Delete Invoice.' }; } +} + +export async function authenticate( + prevState: string | undefined, + formData: FormData, +) { + try { + await signIn('credentials', formData); + } catch (error) { + if (error instanceof AuthError) { + switch (error.type) { + case 'CredentialsSignin': + return 'Invalid credentials.'; + default: + return 'Something went wrong.'; + } + } + throw error; + } } \ No newline at end of file diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..50a147c --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,17 @@ +import AcmeLogo from '@/app/ui/acme-logo'; +import LoginForm from '@/app/ui/login-form'; + +export default function LoginPage() { + return ( +
+
+
+
+ +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/app/ui/dashboard/sidenav.tsx b/app/ui/dashboard/sidenav.tsx index 3d55b46..9da413d 100644 --- a/app/ui/dashboard/sidenav.tsx +++ b/app/ui/dashboard/sidenav.tsx @@ -2,6 +2,7 @@ import Link from 'next/link'; import NavLinks from '@/app/ui/dashboard/nav-links'; import AcmeLogo from '@/app/ui/acme-logo'; import { PowerIcon } from '@heroicons/react/24/outline'; +import { signOut } from '@/auth'; export default function SideNav() { return ( @@ -17,7 +18,10 @@ export default function SideNav() {
-
+ { + 'use server'; + await signOut(); + }}>
+ ); } diff --git a/auth.config.ts b/auth.config.ts new file mode 100644 index 0000000..1266b71 --- /dev/null +++ b/auth.config.ts @@ -0,0 +1,21 @@ +import type { NextAuthConfig } from 'next-auth'; + +export const authConfig = { + pages: { + signIn: '/login', + }, + callbacks: { + authorized({ auth, request: { nextUrl } }) { + const isLoggedIn = !!auth?.user; + const isOnDashboard = nextUrl.pathname.startsWith('/dashboard'); + if (isOnDashboard) { + if (isLoggedIn) return true; + return false; // Redirect unauthenticated users to login page + } else if (isLoggedIn) { + return Response.redirect(new URL('/dashboard', nextUrl)); + } + return true; + }, + }, + providers: [], // Add providers with an empty array for now +} satisfies NextAuthConfig; \ No newline at end of file diff --git a/auth.ts b/auth.ts new file mode 100644 index 0000000..00e7c6f --- /dev/null +++ b/auth.ts @@ -0,0 +1,40 @@ +import NextAuth from 'next-auth'; +import Credentials from 'next-auth/providers/credentials'; +import { authConfig } from './auth.config'; +import { z } from 'zod'; +import { sql } from '@vercel/postgres'; +import type { User } from '@/app/lib/definitions'; +import bcrypt from 'bcrypt'; + +async function getUser(email: string): Promise { + try { + const user = await sql`SELECT * FROM users WHERE email=${email}`; + return user.rows[0]; + } catch (error) { + console.error('Failed to fetch user:', error); + throw new Error('Failed to fetch user.'); + } +} + +export const { auth, signIn, signOut } = NextAuth({ + ...authConfig, + providers: [ + Credentials({ + async authorize(credentials) { + const parsedCredentials = z + .object({ email: z.string().email(), password: z.string().min(6) }) + .safeParse(credentials); + + if (parsedCredentials.success) { + const { email, password } = parsedCredentials.data; + const user = await getUser(email); + if (!user) return null; + const passwordsMatch = await bcrypt.compare(password, user.password); + if (passwordsMatch) return user; + } + console.log('Invalid credentials'); + return null; + }, + }), + ], +}); \ No newline at end of file diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..817a9ff --- /dev/null +++ b/middleware.ts @@ -0,0 +1,9 @@ +import NextAuth from 'next-auth'; +import { authConfig } from './auth.config'; + +export default NextAuth(authConfig).auth; + +export const config = { + // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher + matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5603e66..ce9a8d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "bcrypt": "^5.1.1", "clsx": "^2.0.0", "next": "^14.0.2", + "next-auth": "^5.0.0-beta.4", "postcss": "8.4.31", "react": "18.2.0", "react-dom": "18.2.0", @@ -70,6 +71,27 @@ "node": ">=6.0.0" } }, + "node_modules/@auth/core": { + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.18.4.tgz", + "integrity": "sha512-GsNhsP1xE/3FoNS3dVkPjqRljLNJ4iyL2OLv3klQGNvw3bMpROFcK4lqhx7+pPHiamnVaYt2vg1xbB+lsNaevg==", + "dependencies": { + "@panva/hkdf": "^1.1.1", + "cookie": "0.6.0", + "jose": "^5.1.0", + "oauth4webapi": "^2.3.0", + "preact": "10.11.3", + "preact-render-to-string": "5.2.3" + }, + "peerDependencies": { + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "nodemailer": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -1036,6 +1058,14 @@ "node": ">= 8" } }, + "node_modules/@panva/hkdf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.1.1.tgz", + "integrity": "sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@pkgr/utils": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", @@ -2297,6 +2327,14 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4656,6 +4694,14 @@ "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", "dev": true }, + "node_modules/jose": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.0.tgz", + "integrity": "sha512-oW3PCnvyrcm1HMvGTzqjxxfnEs9EoFOFWi2HsEGhlFVOXxTE3K9GKWVMFoFw06yPUqwpvEWic1BmtUZBI/tIjw==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5057,6 +5103,24 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.4", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.4.tgz", + "integrity": "sha512-vgocjvwPA8gxd/zrIP/vr9lJ/HeNe+C56lPP1D3sdyenHt8KncQV6ro7q0xCsDp1fcOKx7WAWVZH5o8aMxDzgw==", + "dependencies": { + "@auth/core": "0.18.4" + }, + "peerDependencies": { + "next": "^14", + "nodemailer": "^6.6.5", + "react": "^18.2.0" + }, + "peerDependenciesMeta": { + "nodemailer": { + "optional": true + } + } + }, "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", @@ -5185,6 +5249,14 @@ "set-blocking": "^2.0.0" } }, + "node_modules/oauth4webapi": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.4.1.tgz", + "integrity": "sha512-qor45aeDGaGqDOizwut+Q/rZ+J6BIJvOp7U0LtHfbFhg3O7JV5lvQFDXPNqSmcUjuq/Zeq8CIII4RD0sWLOdSQ==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5705,6 +5777,26 @@ "node": ">=0.10.0" } }, + "node_modules/preact": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", + "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5819,6 +5911,11 @@ } } }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/package.json b/package.json index 361f1fc..1be5942 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "bcrypt": "^5.1.1", "clsx": "^2.0.0", "next": "^14.0.2", + "next-auth": "^5.0.0-beta.4", "postcss": "8.4.31", "react": "18.2.0", "react-dom": "18.2.0",