mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-02-13 12:55:42 +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:
90
lib/auth.ts
90
lib/auth.ts
@@ -1,90 +0,0 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import type { NextAuthOptions } from "next-auth";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import { prisma } from "./prisma";
|
||||
|
||||
// Type augmentation moved to types/next-auth.d.ts
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
name: "credentials",
|
||||
credentials: {
|
||||
email: { label: "Email", type: "email" },
|
||||
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 },
|
||||
include: { company: true },
|
||||
});
|
||||
|
||||
if (!user || !user.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(
|
||||
credentials.password,
|
||||
user.password
|
||||
);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if company is active
|
||||
if (user.company.status !== "ACTIVE") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
companyId: user.companyId,
|
||||
role: user.role,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
maxAge: 24 * 60 * 60, // 24 hours for regular users
|
||||
},
|
||||
cookies: {
|
||||
sessionToken: {
|
||||
name: "app-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",
|
||||
};
|
||||
8
lib/auth/client.ts
Normal file
8
lib/auth/client.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createAuthClient } from "@neondatabase/auth/next";
|
||||
|
||||
export const authClient = createAuthClient();
|
||||
|
||||
// Export commonly used hooks for convenience
|
||||
export const { useSession } = authClient;
|
||||
101
lib/auth/server.ts
Normal file
101
lib/auth/server.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { createAuthServer, neonAuth } from "@neondatabase/auth/next/server";
|
||||
import type { UserRole } from "@prisma/client";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const authServer = createAuthServer();
|
||||
|
||||
// Re-export neonAuth for direct use in server components
|
||||
export { neonAuth };
|
||||
|
||||
// Platform roles for easy checking
|
||||
export const PLATFORM_ROLES: UserRole[] = [
|
||||
"PLATFORM_SUPER_ADMIN",
|
||||
"PLATFORM_ADMIN",
|
||||
"PLATFORM_SUPPORT",
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if a role is a platform-level role
|
||||
*/
|
||||
export function isPlatformRole(role: UserRole): boolean {
|
||||
return PLATFORM_ROLES.includes(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user with full data (works for both platform and company users)
|
||||
*/
|
||||
export async function getAuthenticatedUser() {
|
||||
const { session, user: authUser } = await neonAuth();
|
||||
|
||||
if (!session || !authUser?.email) {
|
||||
return { session: null, user: null, authUser: null };
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: authUser.email },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
role: true,
|
||||
companyId: true,
|
||||
company: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return { session, user, authUser };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated company user (for dashboard routes)
|
||||
* Returns null if user is a platform user or has no company
|
||||
*/
|
||||
export async function getAuthenticatedCompanyUser() {
|
||||
const { session, user, authUser } = await getAuthenticatedUser();
|
||||
|
||||
if (!user || !user.companyId || isPlatformRole(user.role)) {
|
||||
return { session: null, user: null, authUser: null };
|
||||
}
|
||||
|
||||
return { session, user, authUser };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated platform user (for platform admin routes)
|
||||
* Returns null if user is not a platform user
|
||||
*/
|
||||
export async function getAuthenticatedPlatformUser() {
|
||||
const { session, user, authUser } = await getAuthenticatedUser();
|
||||
|
||||
if (!user || !isPlatformRole(user.role)) {
|
||||
return { session: null, user: null, authUser: null };
|
||||
}
|
||||
|
||||
return { session, user, authUser };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has specific platform role or higher
|
||||
*/
|
||||
export function hasPlatformAccess(
|
||||
role: UserRole,
|
||||
minRole:
|
||||
| "PLATFORM_SUPPORT"
|
||||
| "PLATFORM_ADMIN"
|
||||
| "PLATFORM_SUPER_ADMIN" = "PLATFORM_SUPPORT"
|
||||
): boolean {
|
||||
const hierarchy: UserRole[] = [
|
||||
"PLATFORM_SUPPORT",
|
||||
"PLATFORM_ADMIN",
|
||||
"PLATFORM_SUPER_ADMIN",
|
||||
];
|
||||
const userLevel = hierarchy.indexOf(role);
|
||||
const minLevel = hierarchy.indexOf(minRole);
|
||||
return userLevel >= minLevel;
|
||||
}
|
||||
25
lib/env.ts
25
lib/env.ts
@@ -73,7 +73,15 @@ try {
|
||||
* Typed environment variables with defaults
|
||||
*/
|
||||
export const env = {
|
||||
// NextAuth
|
||||
// Neon Auth
|
||||
NEON_AUTH_BASE_URL:
|
||||
parseEnvValue(process.env.NEON_AUTH_BASE_URL) ||
|
||||
parseEnvValue(process.env.VITE_NEON_AUTH_URL) ||
|
||||
parseEnvValue(process.env.AUTH_URL) ||
|
||||
"",
|
||||
JWKS_URL: parseEnvValue(process.env.JWKS_URL) || "",
|
||||
|
||||
// Legacy NextAuth (for platform auth - kept for backward compatibility)
|
||||
NEXTAUTH_URL:
|
||||
parseEnvValue(process.env.NEXTAUTH_URL) || "http://localhost:3000",
|
||||
NEXTAUTH_SECRET: parseEnvValue(process.env.NEXTAUTH_SECRET) || "",
|
||||
@@ -106,7 +114,11 @@ export const env = {
|
||||
// Database Configuration
|
||||
DATABASE_URL: parseEnvValue(process.env.DATABASE_URL) || "",
|
||||
DATABASE_URL_DIRECT: parseEnvValue(process.env.DATABASE_URL_DIRECT) || "",
|
||||
|
||||
DATABASE_URL_UNPOOLED:
|
||||
parseEnvValue(process.env.DATABASE_URL_UNPOOLED) ||
|
||||
parseEnvValue(process.env.DATABASE_URLUNPOOLED) ||
|
||||
"",
|
||||
|
||||
// Database Connection Pooling
|
||||
DATABASE_CONNECTION_LIMIT: parseIntWithDefault(
|
||||
process.env.DATABASE_CONNECTION_LIMIT,
|
||||
@@ -131,8 +143,13 @@ export function validateEnv(): { valid: boolean; errors: string[] } {
|
||||
errors.push("DATABASE_URL is required");
|
||||
}
|
||||
|
||||
if (!env.NEXTAUTH_SECRET) {
|
||||
errors.push("NEXTAUTH_SECRET is required");
|
||||
if (!env.NEON_AUTH_BASE_URL) {
|
||||
errors.push("NEON_AUTH_BASE_URL (or AUTH_URL) is required for Neon Auth");
|
||||
}
|
||||
|
||||
// NEXTAUTH_SECRET only required if platform auth is used
|
||||
if (!env.NEXTAUTH_SECRET && env.NODE_ENV === "production") {
|
||||
errors.push("NEXTAUTH_SECRET is required for platform auth in production");
|
||||
}
|
||||
|
||||
if (!env.OPENAI_API_KEY && env.NODE_ENV === "production") {
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import type { NextAuthOptions } from "next-auth";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import { prisma } from "./prisma";
|
||||
|
||||
// Type augmentation moved to types/next-auth.d.ts
|
||||
|
||||
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: "/",
|
||||
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",
|
||||
};
|
||||
@@ -6,21 +6,8 @@ declare const global: {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
// Lazy-load createEnhancedPrismaClient to avoid build-time URL parsing
|
||||
const createPrismaClient = () => {
|
||||
// Use enhanced pooling in production or when explicitly enabled
|
||||
const useEnhancedPooling =
|
||||
process.env.NODE_ENV === "production" ||
|
||||
process.env.USE_ENHANCED_POOLING === "true";
|
||||
|
||||
if (useEnhancedPooling) {
|
||||
console.log("Using enhanced database connection pooling with PG adapter");
|
||||
// Dynamic import to defer URL parsing until runtime
|
||||
const { createEnhancedPrismaClient } = require("./database-pool");
|
||||
return createEnhancedPrismaClient();
|
||||
}
|
||||
|
||||
// Default Prisma client with basic configuration
|
||||
// Create standard Prisma client (no enhanced pooling at build time)
|
||||
const createPrismaClient = (): PrismaClient => {
|
||||
return new PrismaClient({
|
||||
log:
|
||||
process.env.NODE_ENV === "development"
|
||||
@@ -29,6 +16,13 @@ const createPrismaClient = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// Async factory for enhanced pooling (only call at runtime, not build time)
|
||||
export const createEnhancedClient = async (): Promise<PrismaClient> => {
|
||||
const { createEnhancedPrismaClient } = await import("./database-pool");
|
||||
console.log("Using enhanced database connection pooling with PG adapter");
|
||||
return createEnhancedPrismaClient();
|
||||
};
|
||||
|
||||
// Lazy initialization - only create when first accessed
|
||||
let _prisma: PrismaClient | undefined;
|
||||
|
||||
|
||||
17
lib/types.ts
17
lib/types.ts
@@ -1,13 +1,12 @@
|
||||
import type { UserRole } from "@prisma/client";
|
||||
import type { Session as NextAuthSession } from "next-auth";
|
||||
|
||||
export interface UserSession extends Omit<NextAuthSession, "user"> {
|
||||
export interface UserSession {
|
||||
user: {
|
||||
id?: string;
|
||||
id: string;
|
||||
name?: string | null;
|
||||
email?: string;
|
||||
email: string;
|
||||
image?: string | null;
|
||||
companyId: string;
|
||||
companyId: string | null;
|
||||
role: UserRole;
|
||||
};
|
||||
}
|
||||
@@ -26,11 +25,9 @@ export interface Company {
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
password: string;
|
||||
role: string;
|
||||
companyId: string;
|
||||
resetToken?: string | null;
|
||||
resetTokenExpiry?: Date | null;
|
||||
name?: string | null;
|
||||
role: UserRole;
|
||||
companyId: string | null;
|
||||
company?: Company;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
||||
Reference in New Issue
Block a user