Added 4 Demo Users, with Coast emails, Hierarchy, roles etc.
This commit is contained in:
@@ -47,6 +47,7 @@ export default function ChatsPage() {
|
||||
const resizeStartRef = useRef({ x: 0, width: 0 })
|
||||
|
||||
const [conversations, setConversations] = useState(conversationsData)
|
||||
if (!user) return null
|
||||
const conversation = conversations.find((c) => c.id === activeChat)
|
||||
const otherParticipant = (conv: typeof conversationsData[0]) =>
|
||||
conv.participants.find((p) => p.id !== "user1") ?? conv.participants[0]
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import { AppShell } from "@/components/layout/app-shell"
|
||||
import { UserProvider } from "@/providers/user-provider"
|
||||
import { UserProvider, useUser } from "@/providers/user-provider"
|
||||
import { Loader2 } from "lucide-react"
|
||||
|
||||
function DashboardContent({ children }: { children: React.ReactNode }) {
|
||||
const { user, loading } = useUser()
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!user) return null
|
||||
|
||||
return <AppShell>{children}</AppShell>
|
||||
}
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
@@ -10,7 +27,7 @@ export default function DashboardLayout({
|
||||
}) {
|
||||
return (
|
||||
<UserProvider>
|
||||
<AppShell>{children}</AppShell>
|
||||
<DashboardContent>{children}</DashboardContent>
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link"
|
||||
import { motion } from "framer-motion"
|
||||
import { use } from "react"
|
||||
import { PageHeader } from "@/components/shared/page-header"
|
||||
import { LeadDetailsCard } from "@/components/leads/lead-details-card"
|
||||
import { LeadStatusBadge } from "@/components/leads/lead-status-badge"
|
||||
@@ -19,8 +20,9 @@ import { getLeadById } from "@/data/leads"
|
||||
import { getNotesByLeadId } from "@/data/notes"
|
||||
import { ArrowLeft, Edit, ExternalLink } from "lucide-react"
|
||||
|
||||
export default function LeadDetailsPage({ params }: { params: { id: string } }) {
|
||||
const lead = getLeadById(params.id)
|
||||
export default function LeadDetailsPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = use(params)
|
||||
const lead = getLeadById(id)
|
||||
const leadNotes = lead ? getNotesByLeadId(lead.id) : []
|
||||
|
||||
if (!lead) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { toast } from "sonner"
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { user, updateAvatar } = useUser()
|
||||
if (!user) return null
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import {
|
||||
comparePassword,
|
||||
getUserByEmail,
|
||||
getUserByUsername,
|
||||
mapDbUserToSessionUser,
|
||||
recordLoginAttempt,
|
||||
incrementFailedAttempts,
|
||||
resetFailedAttempts,
|
||||
isAccountLocked,
|
||||
createSession,
|
||||
} from "@/lib/auth"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { email, username, password } = await request.json()
|
||||
|
||||
const credential = email || username
|
||||
|
||||
if (!credential || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: "Email/Username and password are required." },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (credential.trim().length === 0 || password.trim().length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "Credentials cannot be empty." },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const ipAddress =
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
||||
request.headers.get("x-real-ip") ||
|
||||
"127.0.0.1"
|
||||
|
||||
const userAgent = request.headers.get("user-agent") || null
|
||||
|
||||
// Try to find user by email first, then by username
|
||||
let dbUser =
|
||||
email || credential.includes("@")
|
||||
? await getUserByEmail(credential)
|
||||
: null
|
||||
|
||||
if (!dbUser) {
|
||||
dbUser = await getUserByUsername(credential)
|
||||
}
|
||||
|
||||
if (!dbUser) {
|
||||
await recordLoginAttempt(
|
||||
null,
|
||||
credential,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
false,
|
||||
"User not found"
|
||||
)
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid email/username or password." },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const lockStatus = await isAccountLocked(dbUser)
|
||||
if (lockStatus.locked) {
|
||||
await recordLoginAttempt(
|
||||
dbUser.id,
|
||||
credential,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
false,
|
||||
lockStatus.reason
|
||||
)
|
||||
return NextResponse.json(
|
||||
{ error: lockStatus.reason },
|
||||
{ status: 423 }
|
||||
)
|
||||
}
|
||||
|
||||
const valid = await comparePassword(password, dbUser.password_hash)
|
||||
if (!valid) {
|
||||
await incrementFailedAttempts(dbUser.id)
|
||||
await recordLoginAttempt(
|
||||
dbUser.id,
|
||||
credential,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
false,
|
||||
"Invalid password"
|
||||
)
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid email/username or password." },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
await resetFailedAttempts(dbUser.id)
|
||||
await recordLoginAttempt(
|
||||
dbUser.id,
|
||||
credential,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
true
|
||||
)
|
||||
|
||||
await createSession(dbUser.id, dbUser.role_name)
|
||||
|
||||
const user = mapDbUserToSessionUser(dbUser)
|
||||
|
||||
return NextResponse.json({ user }, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Login error:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "Authentication service unavailable. Please ensure PostgreSQL is running and configured." },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { destroySession } from "@/lib/auth"
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
await destroySession()
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Logout error:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "Logout failed." },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "Not authenticated." }, { status: 401 })
|
||||
}
|
||||
return NextResponse.json({ user }, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Auth me error:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "Authentication service unavailable." },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
+87
-72
@@ -1,32 +1,107 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Suspense, useState } from "react"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { motion } from "framer-motion"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { COMPANY_NAME } from "@/lib/constants"
|
||||
import { Eye, EyeOff, Loader2 } from "lucide-react"
|
||||
import { Eye, EyeOff, Loader2, AlertCircle } from "lucide-react"
|
||||
|
||||
export default function LoginPage() {
|
||||
function LoginForm() {
|
||||
const router = useRouter()
|
||||
const [email, setEmail] = useState("admin@coastalit.com")
|
||||
const searchParams = useSearchParams()
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [remember, setRemember] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
setLoading(true)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200))
|
||||
try {
|
||||
const res = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password }),
|
||||
})
|
||||
|
||||
router.push("/dashboard")
|
||||
const data = await res.json()
|
||||
|
||||
if (!res.ok) {
|
||||
setError(data.error || "Authentication failed.")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const redirect = searchParams.get("redirect") || "/dashboard"
|
||||
router.push(redirect)
|
||||
} catch {
|
||||
setError("Unable to connect to authentication service. Please ensure the database is running.")
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && (
|
||||
<div className="flex items-start gap-2 rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="name@company.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{loading ? "Signing in..." : "Sign in"}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
{/* Left - Brand panel */}
|
||||
@@ -110,69 +185,9 @@ export default function LoginPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="name@company.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
Forgot password?
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="remember"
|
||||
checked={remember}
|
||||
onCheckedChange={(checked) => setRemember(checked as boolean)}
|
||||
/>
|
||||
<label
|
||||
htmlFor="remember"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{loading ? "Signing in..." : "Sign in"}
|
||||
</Button>
|
||||
</form>
|
||||
<Suspense fallback={<div className="text-center text-muted-foreground py-8">Loading...</div>}>
|
||||
<LoginForm />
|
||||
</Suspense>
|
||||
|
||||
<p className="text-center text-xs text-muted-foreground">
|
||||
By signing in, you agree to our{" "}
|
||||
|
||||
Reference in New Issue
Block a user