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:
2026-01-20 21:09:29 +01:00
parent 7932fe7386
commit cd05fc8648
177 changed files with 5042 additions and 5541 deletions

View File

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

View File

@@ -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") {

View File

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

View File

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

View File

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