diff --git a/package-lock.json b/package-lock.json index 654a613..e5c7320 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "next": "16.1.0", + "next-auth": "^5.0.0-beta.30", "react": "19.2.3", "react-dom": "19.2.3" }, @@ -36,6 +37,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@auth/core": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.0.tgz", + "integrity": "sha512-Wd7mHPQ/8zy6Qj7f4T46vg3aoor8fskJm6g2Zyj064oQ3+p0xNZXAV60ww0hY+MbTesfu29kK14Zk5d5JTazXQ==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1226,6 +1256,15 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -4417,6 +4456,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "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", @@ -5019,6 +5067,33 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.30", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.30.tgz", + "integrity": "sha512-+c51gquM3F6nMVmoAusRJ7RIoY0K4Ts9HCCwyy/BRoe4mp3msZpOzYMyb5LAYc1wSo74PMQkGDcaghIO7W6Xjg==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.0" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", + "nodemailer": "^7.0.7", + "react": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -5054,6 +5129,15 @@ "dev": true, "license": "MIT" }, + "node_modules/oauth4webapi": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.3.tgz", + "integrity": "sha512-pQ5BsX3QRTgnt5HxgHwgunIRaDXBdkT23tf8dfzmtTIL2LTpdmxgbpbBm0VgFWAIDlezQvQCTgnVIUmHupXHxw==", + "license": "MIT", + "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", @@ -5343,6 +5427,25 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index aad4366..aff7627 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "next": "16.1.0", + "next-auth": "^5.0.0-beta.30", "react": "19.2.3", "react-dom": "19.2.3" }, diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..86c9f3d --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/auth"; + +export const { GET, POST } = handlers; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..3194080 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { SessionProvider } from "@/components/providers/session-provider"; import "./globals.css"; const geistSans = Geist({ @@ -13,8 +14,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Real Estate Platform", + description: "Find your dream property with trusted agents", }; export default function RootLayout({ @@ -27,7 +28,7 @@ export default function RootLayout({ - {children} + {children} ); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..1f03186 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { signIn } from "next-auth/react"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +export default function LoginPage() { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const handleCredentialsLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(""); + + const result = await signIn("credentials", { + email, + password, + redirect: false, + }); + + setLoading(false); + + if (result?.error) { + setError("Invalid email or password"); + } else { + router.push("/"); + router.refresh(); + } + }; + + const handleSocialLogin = (provider: "google" | "facebook") => { + signIn(provider, { callbackUrl: "/" }); + }; + + return ( +
+
+
+

+ Real Estate Platform +

+

+ Sign in to your account +

+
+ + {/* SSO Buttons */} +
+ + + +
+ +
+
+
+
+
+ + Or continue with email + +
+
+ + {/* Email/Password Form */} +
+ {error && ( +
+ {error} +
+ )} + +
+ + setEmail(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+ +
+ + setPassword(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+ +
+ + Forgot your password? + +
+ + +
+ +

+ Don't have an account?{" "} + + Sign up + +

+
+
+ ); +} diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..7e469dc --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,131 @@ +import NextAuth from "next-auth"; +import Google from "next-auth/providers/google"; +import Facebook from "next-auth/providers/facebook"; +import Credentials from "next-auth/providers/credentials"; + +export const { handlers, signIn, signOut, auth } = NextAuth({ + providers: [ + // Google OAuth + Google({ + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }), + + // Facebook OAuth + Facebook({ + clientId: process.env.FACEBOOK_CLIENT_ID!, + clientSecret: process.env.FACEBOOK_CLIENT_SECRET!, + }), + + // Email/Password (Credentials) + Credentials({ + name: "credentials", + credentials: { + email: { label: "Email", type: "email" }, + password: { label: "Password", type: "password" }, + }, + async authorize(credentials) { + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/auth/login`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: credentials?.email, + password: credentials?.password, + }), + } + ); + + const data = await res.json(); + + if (res.ok && data.success) { + return { + id: data.data.user.id, + email: data.data.user.email, + name: `${data.data.user.firstName || ""} ${data.data.user.lastName || ""}`.trim(), + role: data.data.user.role, + accessToken: data.data.accessToken, + refreshToken: data.data.refreshToken, + }; + } + + return null; + } catch (error) { + console.error("Auth error:", error); + return null; + } + }, + }), + ], + + pages: { + signIn: "/login", + error: "/login", + }, + + callbacks: { + async signIn({ user, account }) { + // For OAuth providers, sync with backend + if (account?.provider === "google" || account?.provider === "facebook") { + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/auth/social`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + provider: account.provider, + providerId: account.providerAccountId, + email: user.email, + name: user.name, + avatar: user.image, + }), + } + ); + + const data = await res.json(); + + if (res.ok && data.success) { + (user as any).id = data.data.user.id; + (user as any).role = data.data.user.role; + (user as any).accessToken = data.data.accessToken; + (user as any).refreshToken = data.data.refreshToken; + } + } catch (error) { + console.error("Social auth sync error:", error); + } + } + + return true; + }, + + async jwt({ token, user }) { + if (user) { + token.id = user.id; + token.role = (user as any).role; + token.accessToken = (user as any).accessToken; + token.refreshToken = (user as any).refreshToken; + } + return token; + }, + + async session({ session, token }) { + if (session.user) { + (session.user as any).id = token.id; + (session.user as any).role = token.role; + (session.user as any).accessToken = token.accessToken; + (session.user as any).refreshToken = token.refreshToken; + } + return session; + }, + }, + + session: { + strategy: "jwt", + maxAge: 7 * 24 * 60 * 60, // 7 days + }, + + secret: process.env.NEXTAUTH_SECRET, +}); diff --git a/src/components/providers/session-provider.tsx b/src/components/providers/session-provider.tsx new file mode 100644 index 0000000..e194e5f --- /dev/null +++ b/src/components/providers/session-provider.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { SessionProvider as NextAuthSessionProvider } from "next-auth/react"; + +export function SessionProvider({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts new file mode 100644 index 0000000..db7b9e2 --- /dev/null +++ b/src/types/next-auth.d.ts @@ -0,0 +1,27 @@ +import { DefaultSession, DefaultUser } from "next-auth"; + +declare module "next-auth" { + interface Session { + user: { + id: string; + role: "USER" | "AGENT" | "ADMIN"; + accessToken: string; + refreshToken: string; + } & DefaultSession["user"]; + } + + interface User extends DefaultUser { + role: "USER" | "AGENT" | "ADMIN"; + accessToken: string; + refreshToken: string; + } +} + +declare module "next-auth/jwt" { + interface JWT { + id: string; + role: "USER" | "AGENT" | "ADMIN"; + accessToken: string; + refreshToken: string; + } +}