Function bodies 115 total
parseRow function · typescript · L240-L255 (16 LOC)src/lib/nextiva-sync.ts
function parseRow(row: NextivaCSVRow): ParsedPerson {
const rawName = (row.Name || '').trim()
const { firstName, lastName } = parseName(rawName)
return {
firstName,
lastName,
email: (row.Email || '').trim(),
phoneNumber: (row['Phone Number'] || '').trim(),
extension: (row.Extension || '').trim(),
team: (row.Team || '').trim(),
location: normalizeLocation(row.Location || ''),
role: (row.Role || '').trim(),
rawName,
}
}computeFieldChanges function · typescript · L261-L269 (9 LOC)src/lib/nextiva-sync.ts
function computeFieldChanges(
employee: {
extension: string | null
phoneNumber: string | null
location: string
team: string
},
nextiva: ParsedPerson
): Record<string, { old: string; new: string }> {runNextivaSync function · typescript · L316-L506 (191 LOC)src/lib/nextiva-sync.ts
export async function runNextivaSync(
rows: NextivaCSVRow[],
triggeredBy: string,
source: 'manual' | 'automated'
): Promise<SyncResult> {
const result: SyncResult = {
totalRows: rows.length,
matched: 0,
created: 0,
updated: 0,
skipped: 0,
errors: 0,
details: {
autoUpdated: [],
pendingReview: [],
skippedEntries: [],
errorEntries: [],
},
}
// Batch-load all employees and sync mappings upfront to avoid N+1
const [allEmployees, allMappings] = await Promise.all([
prisma.employee.findMany({
select: {
id: true,
firstName: true,
lastName: true,
email: true,
extension: true,
phoneNumber: true,
location: true,
team: true,
},
}),
prisma.syncMapping.findMany(),
])
// Build lookup indexes for fast matching
const employeeByEmail = new Map<string, (typeof allEmployees)[number]>()
for (const emp of allEmployees) {
if (emp.email) {
initializeLiveSearch function · typescript · L18-L91 (74 LOC)src/lib/searchFilter.ts
export function initializeLiveSearch(
searchInputId: string,
employeeCardsSelector: string,
onSearchCallback?: (searchTerm: string, visibleCount: number) => void
): () => void {
const searchInput = document.getElementById(searchInputId) as HTMLInputElement;
if (!searchInput) {
console.error(`Search input with id "${searchInputId}" not found`);
return () => {};
}
let debounceTimer: NodeJS.Timeout;
const handleSearch = () => {
// Clear previous debounce timer
clearTimeout(debounceTimer);
// Debounce the search to avoid excessive filtering
debounceTimer = setTimeout(() => {
const searchTerm = searchInput.value.toLowerCase().trim();
const employeeCards = document.querySelectorAll(employeeCardsSelector) as NodeListOf<SearchableElement>;
let visibleCount = 0;
employeeCards.forEach((card) => {
// Get searchable text from data attribute or card content
const searchableText = card.dataset.searchText || card.texupdateEmptyState function · typescript · L96-L120 (25 LOC)src/lib/searchFilter.ts
function updateEmptyState(visibleCount: number): void {
const gridContainer = document.querySelector('.employee-grid');
if (!gridContainer) return;
// Remove existing empty state
const existingEmptyState = gridContainer.querySelector('.empty-state');
if (existingEmptyState) {
existingEmptyState.remove();
}
// Add empty state if no visible cards
if (visibleCount === 0) {
const emptyState = document.createElement('div');
emptyState.className = 'empty-state';
emptyState.innerHTML = `
<svg class="empty-state-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<h3 class="empty-state-title">No employees found</h3>
<p class="empty-state-description">Try adjusting your search terms</p>
`;
gridContainer.appendChild(emptyState);
}
}clearSearch function · typescript · L125-L132 (8 LOC)src/lib/searchFilter.ts
export function clearSearch(searchInputId: string): void {
const searchInput = document.getElementById(searchInputId) as HTMLInputElement;
if (searchInput) {
searchInput.value = '';
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
}
}prepareEmployeeCards function · typescript · L138-L161 (24 LOC)src/lib/searchFilter.ts
export function prepareEmployeeCards(employeeCardsSelector: string): void {
const employeeCards = document.querySelectorAll(employeeCardsSelector) as NodeListOf<SearchableElement>;
employeeCards.forEach((card) => {
// Extract all text content from name, title, department, etc.
const name = card.querySelector('.employee-name')?.textContent || '';
const title = card.querySelector('.employee-title')?.textContent || '';
const department = card.querySelector('.employee-department')?.textContent || '';
const email = card.querySelector('.employee-email')?.textContent || '';
const location = card.querySelector('.employee-location')?.textContent || '';
// Combine all searchable text
const searchText = [name, title, department, email, location]
.filter(Boolean)
.join(' ')
.toLowerCase();
// Store in data attribute for faster searching
card.dataset.searchText = searchText;
// Ensure card has transition properties
card.styleGenerated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
highlightSearchTerm function · typescript · L166-L193 (28 LOC)src/lib/searchFilter.ts
export function highlightSearchTerm(searchTerm: string, employeeCardsSelector: string): void {
if (!searchTerm) return;
const employeeCards = document.querySelectorAll(employeeCardsSelector);
employeeCards.forEach((card) => {
const textElements = card.querySelectorAll('.employee-name, .employee-title, .employee-department');
textElements.forEach((element) => {
const originalText = element.getAttribute('data-original-text') || element.textContent || '';
// Store original text if not already stored
if (!element.getAttribute('data-original-text')) {
element.setAttribute('data-original-text', originalText);
}
// Create highlighted version
const escaped = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const regex = new RegExp(`(${escaped})`, 'gi');
const highlightedText = originalText.replace(
regex,
'<mark style="background-color: var(--accent-color-light); padding: 2px 4px; border-radius: 3px;">$1<removeHighlighting function · typescript · L198-L211 (14 LOC)src/lib/searchFilter.ts
export function removeHighlighting(employeeCardsSelector: string): void {
const employeeCards = document.querySelectorAll(employeeCardsSelector);
employeeCards.forEach((card) => {
const textElements = card.querySelectorAll('.employee-name, .employee-title, .employee-department');
textElements.forEach((element) => {
const originalText = element.getAttribute('data-original-text');
if (originalText) {
element.textContent = originalText;
}
});
});
}getAllowedIPs function · typescript · L9-L46 (38 LOC)src/middleware.ts
async function getAllowedIPs(requestUrl: string): Promise<string[]> {
const now = Date.now()
if (cachedIPs && now - cacheTimestamp < CACHE_TTL_MS) {
return cachedIPs
}
try {
const origin = new URL(requestUrl).origin
const response = await fetch(
`${origin}/api/internal/allowed-ips`,
{
headers: {
'x-internal-secret': process.env.INTERNAL_API_SECRET || '',
},
}
)
if (!response.ok) {
console.error(
`Failed to fetch allowed IPs: ${response.status} ${response.statusText}`
)
// If we have stale cached data, use it as fallback
if (cachedIPs) return cachedIPs
return []
}
const data = await response.json()
cachedIPs = data.ips ?? []
cacheTimestamp = now
return cachedIPs!
} catch (error) {
console.error('Error fetching allowed IPs:', error)
// Fallback to stale cache if available
if (cachedIPs) return cachedIPs
return []
}
}getClientIP function · typescript · L48-L57 (10 LOC)src/middleware.ts
function getClientIP(request: NextRequest): string | null {
// Vercel sets x-forwarded-for; fall back to x-real-ip
const forwarded = request.headers.get('x-forwarded-for')
if (forwarded) {
// x-forwarded-for can contain multiple IPs; first is the client
return forwarded.split(',')[0].trim()
}
return request.headers.get('x-real-ip')
}blockedResponse function · typescript · L59-L110 (52 LOC)src/middleware.ts
function blockedResponse(): NextResponse {
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Access Restricted</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: #f8fafc;
color: #1e293b;
}
.container {
text-align: center;
max-width: 480px;
padding: 2rem;
}
.icon {
font-size: 3rem;
margin-bottom: 1rem;
}
h1 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.75rem;
}
p {
color: #64748b;
line-height: 1.6;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">🔒</div>
<h1>Access Restricted</h1>
<p>This direisPublicRoute function · typescript · L116-L127 (12 LOC)src/middleware.ts
function isPublicRoute(pathname: string): boolean {
// Root page
if (pathname === '/') return true
// Directory pages
if (pathname.startsWith('/heaton-directory')) return true
// Public employee API (GET) — restrict IPs here; POST/PUT have auth checks
if (pathname === '/api/employees') return true
return false
}isExemptFromIPCheck function · typescript · L133-L153 (21 LOC)src/middleware.ts
function isExemptFromIPCheck(pathname: string): boolean {
// Admin portal — protected by session auth, accessible from any IP
if (pathname.startsWith('/admin')) return true
// Admin API routes — protected by their own auth
if (pathname.startsWith('/api/admin')) return true
// NextAuth API routes — handles Microsoft SSO OIDC flow
if (pathname.startsWith('/api/auth')) return true
// Internal API routes — protected by internal secret
if (pathname.startsWith('/api/internal')) return true
// Next.js internals and static assets
if (pathname.startsWith('/_next')) return true
if (pathname.startsWith('/favicon')) return true
if (pathname === '/robots.txt') return true
if (pathname === '/sitemap.xml') return true
return false
}middleware function · typescript · L155-L185 (31 LOC)src/middleware.ts
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// --- IP Restriction for Public Routes ---
if (!isExemptFromIPCheck(pathname) && isPublicRoute(pathname)) {
const clientIP = getClientIP(request)
if (clientIP) {
const allowedIPs = await getAllowedIPs(request.url)
// If no IPs configured, allow all traffic (fail-open for initial setup)
if (allowedIPs.length > 0 && !allowedIPs.includes(clientIP)) {
return blockedResponse()
}
}
// If we can't determine the client IP, allow the request through
// (better to fail open than block legitimate users)
}
// --- Admin Auth Protection ---
if (pathname.startsWith('/admin') && pathname !== '/admin/login') {
const user = await getSessionFromRequest(request)
if (!user) {
const loginUrl = new URL('/admin/login', request.url)
return NextResponse.redirect(loginUrl)
}
}
return NextResponse.next()
}‹ prevpage 3 / 3