mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-02-13 15:15:45 +01:00
fix: resolve Prettier markdown code block parsing errors
- Fix syntax errors in skills markdown files (.github/skills, .opencode/skills) - Change typescript to tsx for code blocks with JSX - Replace ellipsis (...) in array examples with valid syntax - Separate CSS from TypeScript into distinct code blocks - Convert JavaScript object examples to valid JSON in docs - Fix enum definitions with proper comma separation
This commit is contained in:
@@ -2,16 +2,14 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { checkDatabaseConnection, prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Check if user has admin access (you may want to add proper auth here)
|
||||
const authHeader = request.headers.get("authorization");
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
// Check if user has admin access (you may want to add proper auth here)
|
||||
const authHeader = request.headers.get("authorization");
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
// Basic database connectivity check
|
||||
const isConnected = await checkDatabaseConnection();
|
||||
|
||||
|
||||
@@ -1,44 +1,43 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { fetchAndParseCsv } from "../../../../lib/csvFetcher";
|
||||
import { processQueuedImports } from "../../../../lib/importProcessor";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { neonAuth } from "@/lib/auth/server";
|
||||
import { fetchAndParseCsv } from "@/lib/csvFetcher";
|
||||
import { processQueuedImports } from "@/lib/importProcessor";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
// MIGRATED: Removed "export const dynamic = 'force-dynamic'" - dynamic by default with Cache Components
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
let { companyId } = body;
|
||||
|
||||
if (!companyId) {
|
||||
// Try to get user from prisma based on session cookie
|
||||
try {
|
||||
const session = await prisma.session.findFirst({
|
||||
orderBy: { createdAt: "desc" },
|
||||
where: {
|
||||
/* Add session check criteria here */
|
||||
},
|
||||
});
|
||||
|
||||
if (session) {
|
||||
companyId = session.companyId;
|
||||
}
|
||||
} catch (error) {
|
||||
// Log error for server-side debugging
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
// Use a server-side logging approach instead of console
|
||||
process.stderr.write(`Error fetching session: ${errorMessage}\n`);
|
||||
}
|
||||
// Authenticate user
|
||||
const { session: authSession, user: authUser } = await neonAuth();
|
||||
if (!authSession || !authUser?.email) {
|
||||
return NextResponse.json({ error: "Not logged in" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!companyId) {
|
||||
// Look up user to get companyId and role
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: authUser.email },
|
||||
select: { companyId: true, role: true },
|
||||
});
|
||||
|
||||
if (!user || !user.companyId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Company ID is required" },
|
||||
{ status: 400 }
|
||||
{ error: "User not found or no company" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user has ADMIN role
|
||||
if (user.role !== "ADMIN") {
|
||||
return NextResponse.json(
|
||||
{ error: "Admin access required" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Use user's companyId from auth (ignore any body params for security)
|
||||
const companyId = user.companyId;
|
||||
|
||||
const company = await prisma.company.findUnique({
|
||||
where: { id: companyId },
|
||||
});
|
||||
|
||||
@@ -1,31 +1,20 @@
|
||||
import { ProcessingStage } from "@prisma/client";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { processUnprocessedSessions } from "../../../../lib/processingScheduler";
|
||||
import { getSessionsNeedingProcessing } from "../../../../lib/processingStatusManager";
|
||||
import { neonAuth } from "@/lib/auth/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { processUnprocessedSessions } from "@/lib/processingScheduler";
|
||||
import { getSessionsNeedingProcessing } from "@/lib/processingStatusManager";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
interface SessionUser {
|
||||
email: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface SessionData {
|
||||
user: SessionUser;
|
||||
}
|
||||
// MIGRATED: Removed "export const dynamic = 'force-dynamic'" - dynamic by default with Cache Components
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = (await getServerSession(authOptions)) as SessionData | null;
|
||||
|
||||
if (!session?.user) {
|
||||
const { session: authSession, user: authUser } = await neonAuth();
|
||||
if (!authSession || !authUser?.email) {
|
||||
return NextResponse.json({ error: "Not logged in" }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: session.user.email },
|
||||
where: { email: authUser.email },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import NextAuth from "next-auth";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
|
||||
// Prevent static generation - this route needs runtime env vars
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
3
app/api/auth/[...path]/route.ts
Normal file
3
app/api/auth/[...path]/route.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { authApiHandler } from "@neondatabase/auth/next/server";
|
||||
|
||||
export const { GET, POST } = authApiHandler();
|
||||
23
app/api/auth/me/route.ts
Normal file
23
app/api/auth/me/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getAuthenticatedUser } from "@/lib/auth/server";
|
||||
|
||||
/**
|
||||
* GET /api/auth/me
|
||||
* Returns the current user's data from our User table
|
||||
*/
|
||||
export async function GET() {
|
||||
const { user } = await getAuthenticatedUser();
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
companyId: user.companyId,
|
||||
company: user.company,
|
||||
});
|
||||
}
|
||||
@@ -1,22 +1,21 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { neonAuth } from "@/lib/auth/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
// MIGRATED: Removed "export const dynamic = 'force-dynamic'" - dynamic by default with Cache Components
|
||||
|
||||
export async function GET(_request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) {
|
||||
const { session: authSession, user: authUser } = await neonAuth();
|
||||
if (!authSession || !authUser?.email) {
|
||||
return NextResponse.json({ error: "Not logged in" }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: session.user.email as string },
|
||||
where: { email: authUser.email },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "No user" }, { status: 401 });
|
||||
if (!user || !user.companyId) {
|
||||
return NextResponse.json({ error: "No user or company" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get company data
|
||||
@@ -28,17 +27,17 @@ export async function GET(_request: NextRequest) {
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) {
|
||||
const { session: authSession, user: authUser } = await neonAuth();
|
||||
if (!authSession || !authUser?.email) {
|
||||
return NextResponse.json({ error: "Not logged in" }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: session.user.email as string },
|
||||
where: { email: authUser.email },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "No user" }, { status: 401 });
|
||||
if (!user || !user.companyId) {
|
||||
return NextResponse.json({ error: "No user or company" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { sessionMetrics } from "../../../../lib/metrics";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import type { ChatSession } from "../../../../lib/types";
|
||||
import { neonAuth } from "@/lib/auth/server";
|
||||
import { sessionMetrics } from "@/lib/metrics";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import type { ChatSession } from "@/lib/types";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
interface SessionUser {
|
||||
email: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface SessionData {
|
||||
user: SessionUser;
|
||||
}
|
||||
// MIGRATED: Removed "export const dynamic = 'force-dynamic'" - dynamic by default with Cache Components
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = (await getServerSession(authOptions)) as SessionData | null;
|
||||
if (!session?.user) {
|
||||
const { session: authSession, user: authUser } = await neonAuth();
|
||||
if (!authSession || !authUser?.email) {
|
||||
return NextResponse.json({ error: "Not logged in" }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: session.user.email },
|
||||
where: { email: authUser.email },
|
||||
select: {
|
||||
id: true,
|
||||
companyId: true,
|
||||
@@ -38,8 +28,8 @@ export async function GET(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "No user" }, { status: 401 });
|
||||
if (!user || !user.companyId) {
|
||||
return NextResponse.json({ error: "No user or company" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get date range from query parameters
|
||||
@@ -171,7 +161,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
return NextResponse.json({
|
||||
metrics,
|
||||
csvUrl: user.company.csvUrl,
|
||||
csvUrl: user.company?.csvUrl,
|
||||
company: user.company,
|
||||
dateRange,
|
||||
});
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { neonAuth } from "@/lib/auth/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
// MIGRATED: Removed "export const dynamic = 'force-dynamic'" - dynamic by default with Cache Components
|
||||
|
||||
export async function GET(_request: NextRequest) {
|
||||
const authSession = await getServerSession(authOptions);
|
||||
|
||||
if (!authSession || !authSession.user?.companyId) {
|
||||
const { session: authSession, user: authUser } = await neonAuth();
|
||||
if (!authSession || !authUser?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const companyId = authSession.user.companyId;
|
||||
// Look up user in our database to get companyId
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: authUser.email },
|
||||
select: { companyId: true },
|
||||
});
|
||||
|
||||
if (!user || !user.companyId) {
|
||||
return NextResponse.json(
|
||||
{ error: "User not found or no company" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const companyId = user.companyId;
|
||||
|
||||
try {
|
||||
// Use groupBy for better performance with distinct values
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "../../../../../lib/prisma";
|
||||
import type { ChatSession } from "../../../../../lib/types";
|
||||
import { neonAuth } from "@/lib/auth/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import type { ChatSession } from "@/lib/types";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
// MIGRATED: Removed "export const dynamic = 'force-dynamic'" - dynamic by default with Cache Components
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
@@ -17,6 +18,22 @@ export async function GET(
|
||||
);
|
||||
}
|
||||
|
||||
// Verify user is authenticated
|
||||
const { session: authSession, user: authUser } = await neonAuth();
|
||||
if (!authSession || !authUser?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Look up user in our database to get companyId for authorization
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: authUser.email },
|
||||
select: { companyId: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "User not found" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const prismaSession = await prisma.session.findUnique({
|
||||
where: { id },
|
||||
@@ -31,6 +48,11 @@ export async function GET(
|
||||
return NextResponse.json({ error: "Session not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Verify user has access to this session (same company)
|
||||
if (prismaSession.companyId !== user.companyId) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Map Prisma session object to ChatSession type
|
||||
const session: ChatSession = {
|
||||
// Spread prismaSession to include all its properties
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
import type { Prisma, SessionCategory } from "@prisma/client";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import type { ChatSession } from "../../../../lib/types";
|
||||
import { neonAuth } from "@/lib/auth/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import type { ChatSession } from "@/lib/types";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
// MIGRATED: Removed "export const dynamic = 'force-dynamic'" - dynamic by default with Cache Components
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const authSession = await getServerSession(authOptions);
|
||||
|
||||
if (!authSession || !authSession.user?.companyId) {
|
||||
const { session: authSession, user: authUser } = await neonAuth();
|
||||
if (!authSession || !authUser?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const companyId = authSession.user.companyId;
|
||||
// Look up user in our database to get companyId
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: authUser.email },
|
||||
select: { companyId: true },
|
||||
});
|
||||
|
||||
if (!user || !user.companyId) {
|
||||
return NextResponse.json(
|
||||
{ error: "User not found or no company" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const companyId = user.companyId;
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
const searchTerm = searchParams.get("searchTerm");
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { neonAuth } from "@/lib/auth/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
// MIGRATED: Removed "export const dynamic = 'force-dynamic'" - dynamic by default with Cache Components
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user || session.user.role !== "ADMIN") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
const { session: authSession, user: authUser } = await neonAuth();
|
||||
if (!authSession || !authUser?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Look up user in our database to get companyId and role
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: session.user.email as string },
|
||||
where: { email: authUser.email },
|
||||
select: { companyId: true, role: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "No user" }, { status: 401 });
|
||||
if (!user || !user.companyId) {
|
||||
return NextResponse.json(
|
||||
{ error: "User not found or no company" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check for admin role
|
||||
if (user.role !== "ADMIN") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
import crypto from "node:crypto";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { neonAuth } from "@/lib/auth/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
// MIGRATED: Removed "export const dynamic = 'force-dynamic'" - dynamic by default with Cache Components
|
||||
|
||||
interface UserBasicInfo {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export async function GET(_request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user || session.user.role !== "ADMIN") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
const { session: authSession, user: authUser } = await neonAuth();
|
||||
if (!authSession || !authUser?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Look up user in our database to get companyId and role
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: session.user.email as string },
|
||||
where: { email: authUser.email },
|
||||
select: { companyId: true, role: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "No user" }, { status: 401 });
|
||||
if (!user || !user.companyId) {
|
||||
return NextResponse.json(
|
||||
{ error: "User not found or no company" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check for admin role
|
||||
if (user.role !== "ADMIN") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
@@ -34,28 +42,44 @@ export async function GET(_request: NextRequest) {
|
||||
const mappedUsers: UserBasicInfo[] = users.map((u) => ({
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
name: u.name,
|
||||
role: u.role,
|
||||
}));
|
||||
|
||||
return NextResponse.json({ users: mappedUsers });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/dashboard/users
|
||||
* Invite a new user to the company (creates a placeholder record)
|
||||
* The user will need to sign up via Neon Auth using the same email
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user || session.user.role !== "ADMIN") {
|
||||
const { session: authSession, user: authUser } = await neonAuth();
|
||||
if (!authSession || !authUser?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Look up user in our database to get companyId and role
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: authUser.email },
|
||||
select: { companyId: true, role: true, email: true },
|
||||
});
|
||||
|
||||
if (!user || !user.companyId) {
|
||||
return NextResponse.json(
|
||||
{ error: "User not found or no company" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check for admin role
|
||||
if (user.role !== "ADMIN") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: session.user.email as string },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "No user" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { email, role } = body;
|
||||
const { email, name, role } = body;
|
||||
|
||||
if (!email || !role) {
|
||||
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
|
||||
@@ -63,20 +87,28 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const exists = await prisma.user.findUnique({ where: { email } });
|
||||
if (exists) {
|
||||
return NextResponse.json({ error: "Email exists" }, { status: 409 });
|
||||
return NextResponse.json(
|
||||
{ error: "Email already exists" },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
const tempPassword = crypto.randomBytes(12).toString("base64").slice(0, 12); // secure random initial password
|
||||
|
||||
// Create user record (they'll complete signup via Neon Auth)
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
password: await bcrypt.hash(tempPassword, 10),
|
||||
name: name || null,
|
||||
companyId: user.companyId,
|
||||
role,
|
||||
invitedBy: user.email,
|
||||
invitedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Email user their temp password (stub, for demo) - Implement a robust and secure email sending mechanism. Consider using a transactional email service.
|
||||
return NextResponse.json({ ok: true, tempPassword });
|
||||
// TODO: Send invitation email with sign-up link
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
message:
|
||||
"User invited. They should sign up at /auth/sign-up with this email.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import crypto from "node:crypto";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import { sendEmail } from "../../../lib/sendEmail";
|
||||
import { forgotPasswordSchema, validateInput } from "../../../lib/validation";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// In-memory rate limiting for password reset requests
|
||||
const resetAttempts = new Map<string, { count: number; resetTime: number }>();
|
||||
|
||||
function checkRateLimit(ip: string): boolean {
|
||||
const now = Date.now();
|
||||
const attempts = resetAttempts.get(ip);
|
||||
|
||||
if (!attempts || now > attempts.resetTime) {
|
||||
resetAttempts.set(ip, { count: 1, resetTime: now + 15 * 60 * 1000 }); // 15 minute window
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attempts.count >= 5) {
|
||||
// Max 5 reset requests per 15 minutes per IP
|
||||
return false;
|
||||
}
|
||||
|
||||
attempts.count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Rate limiting check
|
||||
const ip =
|
||||
request.headers.get("x-forwarded-for") ||
|
||||
request.headers.get("x-real-ip") ||
|
||||
"unknown";
|
||||
if (!checkRateLimit(ip)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Too many password reset attempts. Please try again later.",
|
||||
},
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Validate input
|
||||
const validation = validateInput(forgotPasswordSchema, body);
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Invalid email format",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { email } = validation.data;
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
|
||||
// Always return success for privacy (don't reveal if email exists)
|
||||
// But only send email if user exists
|
||||
if (user) {
|
||||
const token = crypto.randomBytes(32).toString("hex");
|
||||
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
|
||||
const expiry = new Date(Date.now() + 1000 * 60 * 30); // 30 min expiry
|
||||
|
||||
await prisma.user.update({
|
||||
where: { email },
|
||||
data: { resetToken: tokenHash, resetTokenExpiry: expiry },
|
||||
});
|
||||
|
||||
const resetUrl = `${process.env.NEXTAUTH_URL || "http://localhost:3000"}/reset-password?token=${token}`;
|
||||
await sendEmail(
|
||||
email,
|
||||
"Password Reset",
|
||||
`Reset your password: ${resetUrl}`
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Forgot password error:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Internal server error",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import NextAuth from "next-auth";
|
||||
import { platformAuthOptions } from "../../../../../lib/platform-auth";
|
||||
|
||||
const handler = NextAuth(platformAuthOptions);
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
@@ -1,10 +1,10 @@
|
||||
import { CompanyStatus } from "@prisma/client";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { platformAuthOptions } from "../../../../../lib/platform-auth";
|
||||
import { prisma } from "../../../../../lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
import {
|
||||
getAuthenticatedPlatformUser,
|
||||
hasPlatformAccess,
|
||||
} from "@/lib/auth/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
// GET /api/platform/companies/[id] - Get company details
|
||||
export async function GET(
|
||||
@@ -12,9 +12,9 @@ export async function GET(
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getServerSession(platformAuthOptions);
|
||||
const { user } = await getAuthenticatedPlatformUser();
|
||||
|
||||
if (!session?.user?.isPlatformUser) {
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: "Platform access required" },
|
||||
{ status: 401 }
|
||||
@@ -67,12 +67,9 @@ export async function PATCH(
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getServerSession(platformAuthOptions);
|
||||
const { user } = await getAuthenticatedPlatformUser();
|
||||
|
||||
if (
|
||||
!session?.user?.isPlatformUser ||
|
||||
session.user.platformRole === "SUPPORT"
|
||||
) {
|
||||
if (!user || !hasPlatformAccess(user.role, "PLATFORM_ADMIN")) {
|
||||
return NextResponse.json(
|
||||
{ error: "Admin access required" },
|
||||
{ status: 403 }
|
||||
@@ -81,12 +78,10 @@ export async function PATCH(
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { name, email, maxUsers, csvUrl, csvUsername, csvPassword, status } =
|
||||
body;
|
||||
const { name, maxUsers, csvUrl, csvUsername, csvPassword, status } = body;
|
||||
|
||||
const updateData: {
|
||||
name?: string;
|
||||
email?: string;
|
||||
maxUsers?: number;
|
||||
csvUrl?: string;
|
||||
csvUsername?: string;
|
||||
@@ -94,7 +89,6 @@ export async function PATCH(
|
||||
status?: CompanyStatus;
|
||||
} = {};
|
||||
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;
|
||||
@@ -122,12 +116,9 @@ export async function DELETE(
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getServerSession(platformAuthOptions);
|
||||
const { user } = await getAuthenticatedPlatformUser();
|
||||
|
||||
if (
|
||||
!session?.user?.isPlatformUser ||
|
||||
session.user.platformRole !== "SUPER_ADMIN"
|
||||
) {
|
||||
if (!user || !hasPlatformAccess(user.role, "PLATFORM_SUPER_ADMIN")) {
|
||||
return NextResponse.json(
|
||||
{ error: "Super admin access required" },
|
||||
{ status: 403 }
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { hash } from "bcryptjs";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { platformAuthOptions } from "../../../../../../lib/platform-auth";
|
||||
import { prisma } from "../../../../../../lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
import {
|
||||
getAuthenticatedPlatformUser,
|
||||
hasPlatformAccess,
|
||||
} from "@/lib/auth/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
// POST /api/platform/companies/[id]/users - Invite user to company
|
||||
export async function POST(
|
||||
@@ -12,11 +11,11 @@ export async function POST(
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getServerSession(platformAuthOptions);
|
||||
const { user: platformUser } = await getAuthenticatedPlatformUser();
|
||||
|
||||
if (
|
||||
!session?.user?.isPlatformUser ||
|
||||
session.user.platformRole === "SUPPORT"
|
||||
!platformUser ||
|
||||
!hasPlatformAccess(platformUser.role, "PLATFORM_ADMIN")
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "Admin access required" },
|
||||
@@ -53,34 +52,26 @@ export async function POST(
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user already exists in this company
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
email,
|
||||
companyId,
|
||||
},
|
||||
// Check if user already exists
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ error: "User already exists in this company" },
|
||||
{ error: "User with this email already exists" },
|
||||
{ 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
|
||||
// Create the user placeholder (they'll complete signup via Neon Auth)
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
name,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
role,
|
||||
companyId,
|
||||
invitedBy: session.user.email,
|
||||
invitedBy: platformUser.email,
|
||||
invitedAt: new Date(),
|
||||
},
|
||||
select: {
|
||||
@@ -94,13 +85,12 @@ export async function POST(
|
||||
},
|
||||
});
|
||||
|
||||
// In a real application, you would send an email with login credentials
|
||||
// For now, we'll return the temporary password
|
||||
// TODO: Send invitation email with sign-up link
|
||||
return NextResponse.json({
|
||||
user,
|
||||
tempPassword, // Remove this in production and send via email
|
||||
signupUrl: "/auth/sign-up",
|
||||
message:
|
||||
"User invited successfully. In production, credentials would be sent via email.",
|
||||
"User invited. They should sign up at /auth/sign-up with this email.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Platform user invitation error:", error);
|
||||
@@ -117,9 +107,9 @@ export async function GET(
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getServerSession(platformAuthOptions);
|
||||
const { user } = await getAuthenticatedPlatformUser();
|
||||
|
||||
if (!session?.user?.isPlatformUser) {
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: "Platform access required" },
|
||||
{ status: 401 }
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { CompanyStatus } from "@prisma/client";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { platformAuthOptions } from "../../../../lib/platform-auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
import {
|
||||
getAuthenticatedPlatformUser,
|
||||
hasPlatformAccess,
|
||||
} from "@/lib/auth/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
// GET /api/platform/companies - List all companies
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(platformAuthOptions);
|
||||
const { user } = await getAuthenticatedPlatformUser();
|
||||
|
||||
if (!session?.user?.isPlatformUser) {
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: "Platform access required" },
|
||||
{ status: 401 }
|
||||
@@ -86,12 +86,9 @@ export async function GET(request: NextRequest) {
|
||||
// POST /api/platform/companies - Create new company
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(platformAuthOptions);
|
||||
const { user } = await getAuthenticatedPlatformUser();
|
||||
|
||||
if (
|
||||
!session?.user?.isPlatformUser ||
|
||||
session.user.platformRole === "SUPPORT"
|
||||
) {
|
||||
if (!user || !hasPlatformAccess(user.role, "PLATFORM_ADMIN")) {
|
||||
return NextResponse.json(
|
||||
{ error: "Admin access required" },
|
||||
{ status: 403 }
|
||||
@@ -106,7 +103,6 @@ export async function POST(request: NextRequest) {
|
||||
csvPassword,
|
||||
adminEmail,
|
||||
adminName,
|
||||
adminPassword,
|
||||
maxUsers = 10,
|
||||
status = "TRIAL",
|
||||
} = body;
|
||||
@@ -125,15 +121,7 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Generate password if not provided
|
||||
const finalAdminPassword =
|
||||
adminPassword || `Temp${Math.random().toString(36).slice(2, 8)}!`;
|
||||
|
||||
// Hash the admin password
|
||||
const bcrypt = await import("bcryptjs");
|
||||
const hashedPassword = await bcrypt.hash(finalAdminPassword, 12);
|
||||
|
||||
// Create company and admin user in a transaction
|
||||
// Create company and admin user placeholder in a transaction
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
// Create the company
|
||||
const company = await tx.company.create({
|
||||
@@ -147,24 +135,19 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
// Create the admin user
|
||||
// Create the admin user placeholder (they'll complete signup via Neon Auth)
|
||||
const adminUser = await tx.user.create({
|
||||
data: {
|
||||
email: adminEmail,
|
||||
password: hashedPassword,
|
||||
name: adminName,
|
||||
role: "ADMIN",
|
||||
companyId: company.id,
|
||||
invitedBy: session.user.email || "platform",
|
||||
invitedBy: user.email,
|
||||
invitedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
company,
|
||||
adminUser,
|
||||
generatedPassword: adminPassword ? null : finalAdminPassword,
|
||||
};
|
||||
return { company, adminUser };
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
@@ -175,7 +158,8 @@ export async function POST(request: NextRequest) {
|
||||
name: result.adminUser.name,
|
||||
role: result.adminUser.role,
|
||||
},
|
||||
generatedPassword: result.generatedPassword,
|
||||
// User should sign up via /auth/sign-up with this email
|
||||
signupUrl: "/auth/sign-up",
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import { registerSchema, validateInput } from "../../../lib/validation";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// In-memory rate limiting (for production, use Redis or similar)
|
||||
const registrationAttempts = new Map<
|
||||
string,
|
||||
{ count: number; resetTime: number }
|
||||
>();
|
||||
|
||||
function checkRateLimit(ip: string): boolean {
|
||||
const now = Date.now();
|
||||
const attempts = registrationAttempts.get(ip);
|
||||
|
||||
if (!attempts || now > attempts.resetTime) {
|
||||
registrationAttempts.set(ip, { count: 1, resetTime: now + 60 * 60 * 1000 }); // 1 hour window
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attempts.count >= 3) {
|
||||
// Max 3 registrations per hour per IP
|
||||
return false;
|
||||
}
|
||||
|
||||
attempts.count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Rate limiting check
|
||||
const ip = request.headers.get("x-forwarded-for") || "unknown";
|
||||
if (!checkRateLimit(ip)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Too many registration attempts. Please try again later.",
|
||||
},
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Validate input with Zod schema
|
||||
const validation = validateInput(registerSchema, body);
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Validation failed",
|
||||
details: validation.errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { email, password, company } = validation.data;
|
||||
|
||||
// Check if email exists
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Email already exists",
|
||||
},
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if company name already exists
|
||||
const existingCompany = await prisma.company.findFirst({
|
||||
where: { name: company },
|
||||
});
|
||||
|
||||
if (existingCompany) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Company name already exists",
|
||||
},
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create company and user in a transaction
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const newCompany = await tx.company.create({
|
||||
data: {
|
||||
name: company,
|
||||
csvUrl: "", // Empty by default, can be set later in settings
|
||||
},
|
||||
});
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 12); // Increased rounds for better security
|
||||
|
||||
const newUser = await tx.user.create({
|
||||
data: {
|
||||
email,
|
||||
password: hashedPassword,
|
||||
companyId: newCompany.id,
|
||||
role: "USER", // Changed from ADMIN - users should be promoted by existing admins
|
||||
},
|
||||
});
|
||||
|
||||
return { company: newCompany, user: newUser };
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: {
|
||||
message: "Registration successful",
|
||||
userId: result.user.id,
|
||||
companyId: result.company.id,
|
||||
},
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Registration error:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Internal server error",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import crypto from "node:crypto";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import { resetPasswordSchema, validateInput } from "../../../lib/validation";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Validate input with strong password requirements
|
||||
const validation = validateInput(resetPasswordSchema, body);
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Validation failed",
|
||||
details: validation.errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { token, password } = validation.data;
|
||||
|
||||
// Hash the token to compare with stored hash
|
||||
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
resetToken: tokenHash,
|
||||
resetTokenExpiry: { gte: new Date() },
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
"Invalid or expired token. Please request a new password reset.",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Hash password with higher rounds for better security
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
resetToken: null,
|
||||
resetTokenExpiry: null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: "Password has been reset successfully.",
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Reset password error:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "An internal server error occurred. Please try again later.",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user