Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions packages/nextjs/app/actions/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"use server"

import { signIn, signOut } from "~~/auth"
import { hash } from "bcryptjs"
import { AuthError } from "next-auth"
import { getDb } from "~~/lib/mongodb"

export async function registerUser(formData: {
name: string
email: string
password: string
role: "investor" | "realtor"
phone?: string
nin?: string
businessName?: string
}) {
try {
const db = await getDb()

const existingUser = await db.collection("users").findOne({
email: formData.email,
})

if (existingUser) {
return { error: "User with this email already exists" }
}

const hashedPassword = await hash(formData.password, 12)

const result = await db.collection("users").insertOne({
name: formData.name,
email: formData.email,
password: hashedPassword,
role: formData.role,
phone: formData.phone || null,
nin: formData.nin || null,
businessName: formData.businessName || null,
createdAt: new Date(),
})

if (!result.insertedId) {
return { error: "Failed to create user" }
}

return { success: true }
} catch (error) {
console.error("Registration error:", error)
return { error: "An error occurred during registration" }
}
}

export async function loginUser(email: string, password: string) {
try {
await signIn("credentials", {
email,
password,
redirect: false,
})

return { success: true }
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case "CredentialsSignin":
return { error: "Invalid email or password" }
default:
return { error: "An error occurred during login" }
}
}
throw error
}
}

export async function logoutUser() {
await signOut({ redirectTo: "/" })
}
43 changes: 43 additions & 0 deletions packages/nextjs/app/actions/verify-credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use server"

import bcrypt from "bcryptjs"
import { z } from "zod"
import { getDb } from "~~/lib/mongodb"

export async function verifyCredentials(email: string, password: string) {
try {
const parsedCredentials = z
.object({ email: z.string().email(), password: z.string().min(6) })
.safeParse({ email, password })

if (!parsedCredentials.success) {
return { success: false, error: "Invalid credentials format" }
}

const db = await getDb()
const user = await db.collection("users").findOne({ email })

if (!user || !user.password) {
return { success: false, error: "Invalid credentials" }
}

const passwordMatch = await bcrypt.compare(password, user.password)

if (!passwordMatch) {
return { success: false, error: "Invalid credentials" }
}

return {
success: true,
user: {
id: user._id.toString(),
email: user.email,
name: user.name,
role: user.role || "investor",
},
}
} catch (error) {
console.error("[v0] Credential verification error:", error)
return { success: false, error: "Authentication failed" }
}
}
3 changes: 3 additions & 0 deletions packages/nextjs/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { handlers } from "~~/auth"

export const { GET, POST } = handlers
62 changes: 62 additions & 0 deletions packages/nextjs/app/dashboard/investor/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { BusinessMetrics } from "~~/components/investor-dashboard/business-metrics"
import { DashboardStats } from "~~/components/investor-dashboard/dashboard-stats"
import { MyProperties } from "~~/components/investor-dashboard/my-properties"
import { PortfolioChart } from "~~/components/investor-dashboard/portfolio-chart"
import { QuickActions } from "~~/components/investor-dashboard/quick-actions"
import { RecentTransactions } from "~~/components/investor-dashboard/recent-transactions"
import { redirect } from "next/navigation"
import { auth } from "~~/auth"
import { SignOutButton } from "~~/components/auth/sign-out-button"

export const metadata = {
title: "Investor Dashboard | reAI",
description: "Manage your tokenized property investments",
}

export default async function InvestorDashboard() {
const session = await auth()

if (!session?.user) {
redirect("/login")
}

if (session.user.role !== "investor") {
redirect("/dashboard/realtor")
}

const userName = session.user.name || "Investor"

return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 mb-2">Welcome Back, {userName}!</h1>
<p className="text-gray-600 text-lg">Track your tokenized property investments and portfolio performance</p>
</div>
<SignOutButton />
</div>

{/* Stats Cards */}
<DashboardStats />

{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-8">
{/* Left Column - 2/3 width */}
<div className="lg:col-span-2 space-y-6">
<PortfolioChart />
<MyProperties />
</div>

{/* Right Column - 1/3 width */}
<div className="space-y-6">
<QuickActions />
<RecentTransactions />
<BusinessMetrics />
</div>
</div>
</div>
</div>
)
}

21 changes: 21 additions & 0 deletions packages/nextjs/app/dashboard/realtor/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client"

import { redirect } from "next/navigation"
import { auth } from "~~/auth"
import { RealtorDashboardClient } from "~~/components/realtor-dashboard/realtor-dashboard-client"

export default async function RealtorDashboard() {
const session = await auth()

if (!session?.user) {
redirect("/login")
}

if (session.user.role !== "realtor") {
redirect("/dashboard/investor")
}

const userName = session.user.name || "Realtor"

return <RealtorDashboardClient userName={userName} />
}
3 changes: 3 additions & 0 deletions packages/nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { ScaffoldStarkAppWithProviders } from "~~/components/ScaffoldStarkAppWithProviders";
import "~~/styles/globals.css";
import { ThemeProvider } from "~~/components/ThemeProvider";
import { Toaster } from "sonner";

export const metadata: Metadata = {
title: "Scaffold-Stark",
Expand All @@ -15,6 +16,8 @@ const ScaffoldStarkApp = ({ children }: { children: React.ReactNode }) => {
<body suppressHydrationWarning>
<ThemeProvider enableSystem>
<ScaffoldStarkAppWithProviders>
<Toaster position="top-right" richColors/>

{children}
</ScaffoldStarkAppWithProviders>
</ThemeProvider>
Expand Down
8 changes: 4 additions & 4 deletions packages/nextjs/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ export const metadata: Metadata = {

export default function LoginPage() {
return (
<div className="min-h-screen bg-white flex items-center justify-center p-4">
<div className="min-h-screen bg-white flex items-center justify-center p-4 sm:p-6 lg:p-8">
<div className="w-full max-w-md">
<div className="relative bg-white border border-gray-200 rounded-2xl p-8 shadow-sm">
<div className="relative bg-white border border-gray-200 rounded-2xl p-6 sm:p-8 shadow-sm">
{/* Close button */}
<Link href="/" className="absolute top-4 right-4 p-2 text-gray-400 hover:text-gray-600 transition-colors">
<X className="w-5 h-5" />
</Link>

{/* Header */}
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Welcome Back</h1>
<p className="text-gray-600">Sign in to your reAI account</p>
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-2">Welcome Back</h1>
<p className="text-sm sm:text-base text-gray-600">Sign in to your reAI account</p>
</div>

{/* Form */}
Expand Down
75 changes: 75 additions & 0 deletions packages/nextjs/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import Google from "next-auth/providers/google"

export const { handlers, auth, signIn, signOut } = NextAuth({
session: {
strategy: "jwt",
},
pages: {
signIn: "/login",
},
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID!,
clientSecret: process.env.AUTH_GOOGLE_SECRET!,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code",
},
},
}),
Credentials({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
id: { label: "ID", type: "text" },
name: { label: "Name", type: "text" },
role: { label: "Role", type: "text" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.id) {
return null
}

const user = credentials as {
id: string
email: string
name: string
role: string
}

return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
}
},
}),
],
callbacks: {
async jwt({ token, user, account }) {
if (user) {
token.role = (user.role as string) || "investor"
token.id = user.id as string
}
if (account?.provider) {
token.provider = account.provider as string
}
return token
},
async session({ session, token }) {
if (session.user) {
session.user.role = (token.role as string) || "investor"
session.user.id = token.id as string
session.user.provider = token.provider as string | undefined
}
return session
},
},
trustHost: true,
})
50 changes: 50 additions & 0 deletions packages/nextjs/components/auth/google-signin-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use client"

import { signIn } from "next-auth/react"
import { Button } from "~~/components/ui/button"
import { useState } from "react"

export function GoogleSignInButton() {
const [isLoading, setIsLoading] = useState(false)

const handleGoogleSignIn = async () => {
try {
setIsLoading(true)
await signIn("google", { callbackUrl: "/dashboard/investor" })
} catch (error) {
console.error("Google sign-in error:", error)
} finally {
setIsLoading(false)
}
}

return (
<Button
type="button"
variant="outline"
className="w-full h-12 bg-transparent"
onClick={handleGoogleSignIn}
disabled={isLoading}
>
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
{isLoading ? "Signing in..." : "Continue with Google"}
</Button>
)
}
Loading
Loading