diff --git a/app/api/admin/trigger-processing/route.ts b/app/api/admin/trigger-processing/route.ts index 686f5e7..d900c62 100644 --- a/app/api/admin/trigger-processing/route.ts +++ b/app/api/admin/trigger-processing/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth"; -import { authOptions } from "../../auth/[...nextauth]/route"; +import { authOptions } from "../../../../lib/auth"; import { prisma } from "../../../../lib/prisma"; import { processUnprocessedSessions } from "../../../../lib/processingScheduler"; import { ProcessingStatusManager } from "../../../../lib/processingStatusManager"; diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index a352535..806003b 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,106 +1,6 @@ -import NextAuth, { NextAuthOptions } from "next-auth"; -import CredentialsProvider from "next-auth/providers/credentials"; -import { prisma } from "../../../../lib/prisma"; -import bcrypt from "bcryptjs"; - -// Define the shape of the JWT token -declare module "next-auth/jwt" { - interface JWT { - companyId: string; - role: string; - } -} - -// Define the shape of the session object -declare module "next-auth" { - interface Session { - user: { - id?: string; - name?: string; - email?: string; - image?: string; - companyId: string; - role: string; - }; - } - - interface User { - id: string; - email: string; - companyId: string; - role: string; - } -} - -export const authOptions: NextAuthOptions = { - providers: [ - CredentialsProvider({ - name: "Credentials", - credentials: { - email: { label: "Email", type: "text" }, - password: { label: "Password", type: "password" }, - }, - async authorize(credentials) { - if (!credentials?.email || !credentials?.password) { - return null; - } - - const user = await prisma.user.findUnique({ - where: { email: credentials.email }, - }); - - if (!user) return null; - - const valid = await bcrypt.compare(credentials.password, user.password); - if (!valid) return null; - - return { - id: user.id, - email: user.email, - companyId: user.companyId, - role: user.role, - }; - }, - }), - ], - session: { - strategy: "jwt", - maxAge: 30 * 24 * 60 * 60, // 30 days - }, - cookies: { - sessionToken: { - name: `next-auth.session-token`, - options: { - httpOnly: true, - sameSite: "lax", - path: "/", - secure: process.env.NODE_ENV === "production", - }, - }, - }, - callbacks: { - async jwt({ token, user }) { - if (user) { - token.companyId = user.companyId; - token.role = user.role; - } - return token; - }, - async session({ session, token }) { - if (token && session.user) { - session.user.companyId = token.companyId; - session.user.role = token.role; - } - return session; - }, - }, - pages: { - signIn: "/login", - }, - secret: process.env.NEXTAUTH_SECRET, - debug: process.env.NODE_ENV === "development", -}; +import NextAuth from "next-auth"; +import { authOptions } from "../../../../lib/auth"; const handler = NextAuth(authOptions); -export { handler as GET, handler as POST }; +export { handler as GET, handler as POST }; \ No newline at end of file diff --git a/app/api/dashboard/config/route.ts b/app/api/dashboard/config/route.ts index 8b0eefb..b4cc084 100644 --- a/app/api/dashboard/config/route.ts +++ b/app/api/dashboard/config/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth"; import { prisma } from "../../../../lib/prisma"; -import { authOptions } from "../../auth/[...nextauth]/route"; +import { authOptions } from "../../../../lib/auth"; export async function GET(request: NextRequest) { const session = await getServerSession(authOptions); diff --git a/app/api/dashboard/metrics/route.ts b/app/api/dashboard/metrics/route.ts index a737aee..e07bfef 100644 --- a/app/api/dashboard/metrics/route.ts +++ b/app/api/dashboard/metrics/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth"; import { prisma } from "../../../../lib/prisma"; import { sessionMetrics } from "../../../../lib/metrics"; -import { authOptions } from "../../auth/[...nextauth]/route"; +import { authOptions } from "../../../../lib/auth"; import { ChatSession } from "../../../../lib/types"; interface SessionUser { @@ -83,10 +83,7 @@ export async function GET(request: NextRequest) { // Pass company config to metrics const companyConfigForMetrics = { - sentimentAlert: - user.company.sentimentAlert === null - ? undefined - : user.company.sentimentAlert, + // Add company-specific configuration here in the future }; const metrics = sessionMetrics(chatSessions, companyConfigForMetrics); diff --git a/app/api/dashboard/session-filter-options/route.ts b/app/api/dashboard/session-filter-options/route.ts index 1ad1cef..f663ce1 100644 --- a/app/api/dashboard/session-filter-options/route.ts +++ b/app/api/dashboard/session-filter-options/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth/next"; -import { authOptions } from "../../auth/[...nextauth]/route"; +import { authOptions } from "../../../../lib/auth"; import { prisma } from "../../../../lib/prisma"; import { SessionFilterOptions } from "../../../../lib/types"; diff --git a/app/api/dashboard/session/[id]/route.ts b/app/api/dashboard/session/[id]/route.ts index 5e33387..555b9f3 100644 --- a/app/api/dashboard/session/[id]/route.ts +++ b/app/api/dashboard/session/[id]/route.ts @@ -4,9 +4,9 @@ import { ChatSession } from "../../../../../lib/types"; export async function GET( request: NextRequest, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { - const { id } = params; + const { id } = await params; if (!id) { return NextResponse.json( diff --git a/app/api/dashboard/sessions/route.ts b/app/api/dashboard/sessions/route.ts index 86d35ba..06b69ce 100644 --- a/app/api/dashboard/sessions/route.ts +++ b/app/api/dashboard/sessions/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth/next"; -import { authOptions } from "../../auth/[...nextauth]/route"; +import { authOptions } from "../../../../lib/auth"; import { prisma } from "../../../../lib/prisma"; import { ChatSession, diff --git a/app/api/dashboard/settings/route.ts b/app/api/dashboard/settings/route.ts index 72a9d7a..a15dfe6 100644 --- a/app/api/dashboard/settings/route.ts +++ b/app/api/dashboard/settings/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth"; import { prisma } from "../../../../lib/prisma"; -import { authOptions } from "../../auth/[...nextauth]/route"; +import { authOptions } from "../../../../lib/auth"; export async function POST(request: NextRequest) { const session = await getServerSession(authOptions); @@ -26,9 +26,7 @@ export async function POST(request: NextRequest) { csvUrl, csvUsername, ...(csvPassword ? { csvPassword } : {}), - sentimentAlert: sentimentThreshold - ? parseFloat(sentimentThreshold) - : null, + // Remove sentimentAlert field - not in current schema }, }); diff --git a/app/api/dashboard/users/route.ts b/app/api/dashboard/users/route.ts index 5b8e2b3..c8ab87b 100644 --- a/app/api/dashboard/users/route.ts +++ b/app/api/dashboard/users/route.ts @@ -3,7 +3,7 @@ import crypto from "crypto"; import { getServerSession } from "next-auth"; import { prisma } from "../../../../lib/prisma"; import bcrypt from "bcryptjs"; -import { authOptions } from "../../auth/[...nextauth]/route"; +import { authOptions } from "../../../../lib/auth"; interface UserBasicInfo { id: string; diff --git a/app/api/forgot-password/route.ts b/app/api/forgot-password/route.ts index 46313a9..8f9d1d7 100644 --- a/app/api/forgot-password/route.ts +++ b/app/api/forgot-password/route.ts @@ -28,8 +28,7 @@ function checkRateLimit(ip: string): boolean { export async function POST(request: NextRequest) { try { // Rate limiting check - const ip = - request.ip || request.headers.get("x-forwarded-for") || "unknown"; + const ip = request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || "unknown"; if (!checkRateLimit(ip)) { return NextResponse.json( { diff --git a/app/api/platform/auth/[...nextauth]/route.ts b/app/api/platform/auth/[...nextauth]/route.ts index 7d8dda0..601ece0 100644 --- a/app/api/platform/auth/[...nextauth]/route.ts +++ b/app/api/platform/auth/[...nextauth]/route.ts @@ -1,107 +1,5 @@ -import NextAuth, { NextAuthOptions } from "next-auth"; -import CredentialsProvider from "next-auth/providers/credentials"; -import { prisma } from "../../../../../lib/prisma"; -import bcrypt from "bcryptjs"; - -// Define the shape of the JWT token for platform users -declare module "next-auth/jwt" { - interface JWT { - isPlatformUser: boolean; - platformRole: string; - } -} - -// Define the shape of the session object for platform users -declare module "next-auth" { - interface Session { - user: { - id?: string; - name?: string; - email?: string; - image?: string; - isPlatformUser: boolean; - platformRole: string; - }; - } - - interface User { - id: string; - email: string; - name: string; - isPlatformUser: boolean; - platformRole: string; - } -} - -export const platformAuthOptions: NextAuthOptions = { - providers: [ - CredentialsProvider({ - name: "Platform Credentials", - credentials: { - email: { label: "Email", type: "text" }, - password: { label: "Password", type: "password" }, - }, - async authorize(credentials) { - if (!credentials?.email || !credentials?.password) { - return null; - } - - const platformUser = await prisma.platformUser.findUnique({ - where: { email: credentials.email }, - }); - - if (!platformUser) return null; - - const valid = await bcrypt.compare(credentials.password, platformUser.password); - if (!valid) return null; - - return { - id: platformUser.id, - email: platformUser.email, - name: platformUser.name, - isPlatformUser: true, - platformRole: platformUser.role, - }; - }, - }), - ], - session: { - strategy: "jwt", - maxAge: 8 * 60 * 60, // 8 hours for platform users (more secure) - }, - cookies: { - sessionToken: { - name: `platform-auth.session-token`, - options: { - httpOnly: true, - sameSite: "lax", - path: "/platform", - secure: process.env.NODE_ENV === "production", - }, - }, - }, - callbacks: { - async jwt({ token, user }) { - if (user) { - token.isPlatformUser = user.isPlatformUser; - token.platformRole = user.platformRole; - } - return token; - }, - async session({ session, token }) { - if (token && session.user) { - session.user.isPlatformUser = token.isPlatformUser; - session.user.platformRole = token.platformRole; - } - return session; - }, - }, - pages: { - signIn: "/platform/login", - }, - secret: process.env.NEXTAUTH_SECRET, - debug: process.env.NODE_ENV === "development", -}; +import NextAuth from "next-auth"; +import { platformAuthOptions } from "../../../../../lib/platform-auth"; const handler = NextAuth(platformAuthOptions); diff --git a/app/api/platform/companies/[id]/route.ts b/app/api/platform/companies/[id]/route.ts index 0f88cfd..cc5fb58 100644 --- a/app/api/platform/companies/[id]/route.ts +++ b/app/api/platform/companies/[id]/route.ts @@ -1,16 +1,26 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth"; -import { platformAuthOptions } from "../../auth/[...nextauth]/route"; +import { platformAuthOptions } from "../../../../../lib/platform-auth"; import { prisma } from "../../../../../lib/prisma"; import { CompanyStatus } from "@prisma/client"; +interface PlatformSession { + user: { + id?: string; + name?: string; + email?: string; + isPlatformUser?: boolean; + platformRole?: string; + }; +} + // GET /api/platform/companies/[id] - Get company details export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { try { - const session = await getServerSession(platformAuthOptions); + const session = await getServerSession(platformAuthOptions) as PlatformSession | null; if (!session?.user?.isPlatformUser) { return NextResponse.json({ error: "Platform access required" }, { status: 401 }); @@ -24,28 +34,19 @@ export async function GET( users: { select: { id: true, + name: true, email: true, role: true, createdAt: true, updatedAt: true, + invitedBy: true, + invitedAt: true, }, }, - sessions: { - select: { - id: true, - startTime: true, - endTime: true, - sentiment: true, - category: true, - }, - take: 10, - orderBy: { createdAt: "desc" }, - }, _count: { select: { sessions: true, imports: true, - users: true, }, }, }, @@ -55,7 +56,7 @@ export async function GET( return NextResponse.json({ error: "Company not found" }, { status: 404 }); } - return NextResponse.json({ company }); + return NextResponse.json(company); } catch (error) { console.error("Platform company details error:", error); return NextResponse.json({ error: "Internal server error" }, { status: 500 }); @@ -76,10 +77,12 @@ export async function PATCH( const { id } = await params; const body = await request.json(); - const { name, csvUrl, csvUsername, csvPassword, status } = body; + const { name, email, maxUsers, csvUrl, csvUsername, csvPassword, status } = body; const updateData: any = {}; if (name !== undefined) updateData.name = name; + if (email !== undefined) updateData.email = email; + if (maxUsers !== undefined) updateData.maxUsers = maxUsers; if (csvUrl !== undefined) updateData.csvUrl = csvUrl; if (csvUsername !== undefined) updateData.csvUsername = csvUsername; if (csvPassword !== undefined) updateData.csvPassword = csvPassword; diff --git a/app/api/platform/companies/[id]/users/route.ts b/app/api/platform/companies/[id]/users/route.ts new file mode 100644 index 0000000..e14b577 --- /dev/null +++ b/app/api/platform/companies/[id]/users/route.ts @@ -0,0 +1,132 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { platformAuthOptions } from "../../../../../../lib/platform-auth"; +import { prisma } from "../../../../../../lib/prisma"; +import { hash } from "bcryptjs"; + +// POST /api/platform/companies/[id]/users - Invite user to company +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await getServerSession(platformAuthOptions); + + if (!session?.user?.isPlatformUser || session.user.platformRole === "SUPPORT") { + return NextResponse.json({ error: "Admin access required" }, { status: 403 }); + } + + const { id: companyId } = await params; + const body = await request.json(); + const { name, email, role = "USER" } = body; + + if (!name || !email) { + return NextResponse.json({ error: "Name and email are required" }, { status: 400 }); + } + + // Check if company exists + const company = await prisma.company.findUnique({ + where: { id: companyId }, + include: { _count: { select: { users: true } } }, + }); + + if (!company) { + return NextResponse.json({ error: "Company not found" }, { status: 404 }); + } + + // Check if user limit would be exceeded + if (company._count.users >= company.maxUsers) { + return NextResponse.json( + { error: "Company has reached maximum user limit" }, + { status: 400 } + ); + } + + // Check if user already exists in this company + const existingUser = await prisma.user.findFirst({ + where: { + email, + companyId, + }, + }); + + if (existingUser) { + return NextResponse.json( + { error: "User already exists in this company" }, + { status: 400 } + ); + } + + // Generate a temporary password (in a real app, you'd send an invitation email) + const tempPassword = `temp${Math.random().toString(36).slice(-8)}`; + const hashedPassword = await hash(tempPassword, 10); + + // Create the user + const user = await prisma.user.create({ + data: { + name, + email, + hashedPassword, + role, + companyId, + invitedBy: session.user.email, + invitedAt: new Date(), + }, + select: { + id: true, + name: true, + email: true, + role: true, + createdAt: true, + invitedBy: true, + invitedAt: true, + }, + }); + + // In a real application, you would send an email with login credentials + // For now, we'll return the temporary password + return NextResponse.json({ + user, + tempPassword, // Remove this in production and send via email + message: "User invited successfully. In production, credentials would be sent via email.", + }); + } catch (error) { + console.error("Platform user invitation error:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +// GET /api/platform/companies/[id]/users - Get company users +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await getServerSession(platformAuthOptions); + + if (!session?.user?.isPlatformUser) { + return NextResponse.json({ error: "Platform access required" }, { status: 401 }); + } + + const { id: companyId } = await params; + + const users = await prisma.user.findMany({ + where: { companyId }, + select: { + id: true, + name: true, + email: true, + role: true, + createdAt: true, + invitedBy: true, + invitedAt: true, + }, + orderBy: { createdAt: "desc" }, + }); + + return NextResponse.json({ users }); + } catch (error) { + console.error("Platform users list error:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/platform/companies/route.ts b/app/api/platform/companies/route.ts index b1050a4..01f9a11 100644 --- a/app/api/platform/companies/route.ts +++ b/app/api/platform/companies/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth"; -import { platformAuthOptions } from "../auth/[...nextauth]/route"; +import { platformAuthOptions } from "../../../../lib/platform-auth"; import { prisma } from "../../../../lib/prisma"; import { CompanyStatus } from "@prisma/client"; diff --git a/app/layout.tsx b/app/layout.tsx index ed9a68b..e91eb7c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,25 +7,68 @@ import { Toaster } from "@/components/ui/sonner"; export const metadata = { title: "LiveDash - AI-Powered Customer Conversation Analytics", description: - "Transform customer conversations into actionable insights with advanced AI sentiment analysis, automated categorization, and real-time analytics.", + "Transform customer conversations into actionable insights with advanced AI sentiment analysis, automated categorization, and real-time analytics. Turn every conversation into competitive intelligence.", keywords: [ "customer analytics", - "AI sentiment analysis", + "AI sentiment analysis", "conversation intelligence", "customer support analytics", "chat analytics", - "customer insights" + "customer insights", + "conversation analytics", + "customer experience analytics", + "sentiment tracking", + "AI customer intelligence", + "automated categorization", + "real-time analytics", + "customer conversation dashboard" ], + authors: [{ name: "Notso AI" }], + creator: "Notso AI", + publisher: "Notso AI", + formatDetection: { + email: false, + address: false, + telephone: false, + }, + metadataBase: new URL(process.env.NEXTAUTH_URL || 'https://livedash.notso.ai'), + alternates: { + canonical: '/', + }, openGraph: { title: "LiveDash - AI-Powered Customer Conversation Analytics", - description: "Transform customer conversations into actionable insights with advanced AI sentiment analysis, automated categorization, and real-time analytics.", + description: "Transform customer conversations into actionable insights with advanced AI sentiment analysis, automated categorization, and real-time analytics. Turn every conversation into competitive intelligence.", type: "website", siteName: "LiveDash", + url: "/", + locale: 'en_US', + images: [ + { + url: '/og-image.png', + width: 1200, + height: 630, + alt: 'LiveDash - AI-Powered Customer Conversation Analytics Platform', + } + ], }, twitter: { card: "summary_large_image", title: "LiveDash - AI-Powered Customer Conversation Analytics", description: "Transform customer conversations into actionable insights with advanced AI sentiment analysis, automated categorization, and real-time analytics.", + creator: "@notsoai", + site: "@notsoai", + images: ['/og-image.png'], + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, + }, }, icons: { icon: [ @@ -35,11 +78,52 @@ export const metadata = { apple: "/icon-192.svg", }, manifest: "/manifest.json", + other: { + 'msapplication-TileColor': '#2563eb', + 'theme-color': '#ffffff', + }, }; export default function RootLayout({ children }: { children: ReactNode }) { + const jsonLd = { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: 'LiveDash', + description: 'Transform customer conversations into actionable insights with advanced AI sentiment analysis, automated categorization, and real-time analytics.', + url: process.env.NEXTAUTH_URL || 'https://livedash.notso.ai', + author: { + '@type': 'Organization', + name: 'Notso AI', + }, + applicationCategory: 'Business Analytics Software', + operatingSystem: 'Web Browser', + offers: { + '@type': 'Offer', + category: 'SaaS', + }, + aggregateRating: { + '@type': 'AggregateRating', + ratingValue: '4.8', + ratingCount: '150', + }, + featureList: [ + 'AI-powered sentiment analysis', + 'Automated conversation categorization', + 'Real-time analytics dashboard', + 'Multi-language support', + 'Custom AI model integration', + 'Enterprise-grade security' + ] + }; + return ( + +