feat: complete platform management features and enhance SEO

- Add comprehensive company management interface with editing, suspension
- Implement user invitation system within companies
- Add Add Company modal with form validation
- Create platform auth configuration in separate lib file
- Add comprehensive SEO metadata with OpenGraph and structured data
- Fix auth imports and route exports for Next.js 15 compatibility
- Add toast notifications with RadixUI components
- Update TODO status to reflect 100% completion of platform features
This commit is contained in:
2025-06-28 13:20:48 +02:00
parent fdb1a9c2b1
commit 1972c5e9f7
26 changed files with 1627 additions and 251 deletions

View File

@ -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";

View File

@ -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 };

View File

@ -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);

View File

@ -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);

View File

@ -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";

View File

@ -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(

View File

@ -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,

View File

@ -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
},
});

View File

@ -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;

View File

@ -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(
{

View File

@ -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);

View File

@ -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;

View File

@ -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 });
}
}

View File

@ -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";