v1.0.0-alpha4: Add Redis/Valkey caching and search with principal agent auth for global settings
This commit is contained in:
@@ -4,6 +4,7 @@ import { serveStatic } from 'hono/bun';
|
||||
import { cors } from 'hono/cors';
|
||||
import PocketBase from 'pocketbase';
|
||||
import { ConfidentialClientApplication } from '@azure/msal-node';
|
||||
import { getCached, setCached, deleteCached, getCachedSearchResults, cacheSearchResults, invalidateSearchCache } from './redis';
|
||||
|
||||
// Load secrets from absolute path (not in project directory)
|
||||
config({ path: '/home/admin/secrets/.env' });
|
||||
@@ -75,6 +76,11 @@ app.post('/api/submit', async (c) => {
|
||||
submittedAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Invalidate cache after update
|
||||
const collectionName = process.env.PB_COLLECTION || 'Job_Info_TestEnv';
|
||||
await deleteCached(`jobs:*`);
|
||||
await invalidateSearchCache();
|
||||
|
||||
console.log('submission_success', { recordId: record.id });
|
||||
return c.json({ success: true, message: 'Feedback submitted successfully', recordId: record.id });
|
||||
} catch (err) {
|
||||
@@ -119,6 +125,32 @@ app.post('/api/admin/login', async (c) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get environment variable for principal agent (admin only)
|
||||
app.get('/api/admin/env', async (c) => {
|
||||
try {
|
||||
const key = c.req.query('key');
|
||||
if (!key) {
|
||||
return c.json({ message: 'Missing key parameter' }, 400);
|
||||
}
|
||||
|
||||
// Only allow specific agent credentials
|
||||
const allowedKeys = ['PB_AGENT_EMAIL', 'PB_AGENT_PASSWORD'];
|
||||
if (!allowedKeys.includes(key)) {
|
||||
return c.json({ message: 'Unauthorized key' }, 403);
|
||||
}
|
||||
|
||||
const value = process.env[key];
|
||||
if (!value) {
|
||||
return c.json({ message: 'Key not found' }, 404);
|
||||
}
|
||||
|
||||
return c.json({ ok: true, value });
|
||||
} catch (err) {
|
||||
console.error('env_fetch_error', { message: (err as Error)?.message });
|
||||
return c.json({ message: 'Internal error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Get collection schema (requires admin token)
|
||||
app.get('/api/schema', async (c) => {
|
||||
try {
|
||||
@@ -468,11 +500,154 @@ app.get('/api/export/schema', async (c) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get all jobs with Redis caching
|
||||
// Note: Caches the full unfiltered dataset - all users share this cache
|
||||
// Filtering/sorting/column preferences are handled client-side per user
|
||||
app.get('/api/jobs', async (c) => {
|
||||
try {
|
||||
const pbToken = c.req.header('X-PocketBase-Token');
|
||||
if (!pbToken) {
|
||||
return c.json({ success: false, message: 'Missing authentication token' }, 401);
|
||||
}
|
||||
|
||||
// Validate user token
|
||||
setUserPocketBaseAuth(pbToken);
|
||||
try {
|
||||
await pb.collection(process.env.PB_AUTH_COLLECTION || 'Users').authRefresh();
|
||||
} catch (e) {
|
||||
console.error('auth_validation_failed', { message: (e as Error)?.message });
|
||||
return c.json({ success: false, message: 'Invalid token' }, 401);
|
||||
}
|
||||
|
||||
const collectionName = process.env.PB_COLLECTION || 'Job_Info_TestEnv';
|
||||
// Shared cache key for all users - full dataset, no filters applied
|
||||
const cacheKey = `jobs:all:${collectionName}`;
|
||||
|
||||
// Try to get from cache first
|
||||
const cached = await getCached<any[]>(cacheKey);
|
||||
if (cached) {
|
||||
console.log('Jobs fetched from cache');
|
||||
return c.json({ success: true, data: cached, cached: true });
|
||||
}
|
||||
|
||||
// Fetch from PocketBase if not in cache
|
||||
const records = await pb.collection(collectionName).getFullList({
|
||||
sort: 'Job_Number',
|
||||
});
|
||||
|
||||
// Cache for 5 minutes (300 seconds)
|
||||
await setCached(cacheKey, records, { ttl: 300 });
|
||||
|
||||
console.log('Jobs fetched from PocketBase and cached');
|
||||
return c.json({ success: true, data: records, cached: false });
|
||||
} catch (err) {
|
||||
console.error('jobs_fetch_error', { message: (err as Error)?.message });
|
||||
return c.json({ success: false, message: 'Failed to fetch jobs' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Search jobs with Redis caching
|
||||
app.get('/api/jobs/search', async (c) => {
|
||||
try {
|
||||
const pbToken = c.req.header('X-PocketBase-Token');
|
||||
if (!pbToken) {
|
||||
return c.json({ success: false, message: 'Missing authentication token' }, 401);
|
||||
}
|
||||
|
||||
// Validate user token
|
||||
setUserPocketBaseAuth(pbToken);
|
||||
try {
|
||||
await pb.collection(process.env.PB_AUTH_COLLECTION || 'Users').authRefresh();
|
||||
} catch (e) {
|
||||
console.error('auth_validation_failed', { message: (e as Error)?.message });
|
||||
return c.json({ success: false, message: 'Invalid token' }, 401);
|
||||
}
|
||||
|
||||
const url = new URL(c.req.url);
|
||||
const query = url.searchParams.get('q') || '';
|
||||
|
||||
if (!query.trim()) {
|
||||
return c.json({ success: false, message: 'Search query is required' }, 400);
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
const cached = await getCachedSearchResults(query);
|
||||
if (cached) {
|
||||
console.log('Search results fetched from cache');
|
||||
return c.json({ success: true, data: cached, query, cached: true });
|
||||
}
|
||||
|
||||
const collectionName = process.env.PB_COLLECTION || 'Job_Info_TestEnv';
|
||||
|
||||
// Build search filter for PocketBase
|
||||
// Search across key fields: Job_Number, Job_Full_Name, Job_Name, Company_Client, Contact_Person, Estimator
|
||||
const searchTerm = query.trim();
|
||||
const filter = [
|
||||
`Job_Number ~ "${searchTerm}"`,
|
||||
`Job_Full_Name ~ "${searchTerm}"`,
|
||||
`Job_Name ~ "${searchTerm}"`,
|
||||
`Company_Client ~ "${searchTerm}"`,
|
||||
`Contact_Person ~ "${searchTerm}"`,
|
||||
`Estimator ~ "${searchTerm}"`,
|
||||
`Job_Status ~ "${searchTerm}"`,
|
||||
`Project_Manager ~ "${searchTerm}"`,
|
||||
`Notes ~ "${searchTerm}"`
|
||||
].join(' || ');
|
||||
|
||||
const results = await pb.collection(collectionName).getFullList({
|
||||
filter,
|
||||
sort: 'Job_Number',
|
||||
});
|
||||
|
||||
// Cache search results for 3 minutes (180 seconds)
|
||||
await cacheSearchResults(query, results, { ttl: 180 });
|
||||
|
||||
console.log('Search results fetched from PocketBase and cached', { query, count: results.length });
|
||||
return c.json({ success: true, data: results, query, cached: false });
|
||||
} catch (err) {
|
||||
console.error('search_error', { message: (err as Error)?.message });
|
||||
return c.json({ success: false, message: 'Search failed' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear cache endpoint (optional - for admin use)
|
||||
app.post('/api/cache/clear', async (c) => {
|
||||
try {
|
||||
const pbToken = c.req.header('X-PocketBase-Token');
|
||||
if (!pbToken) {
|
||||
return c.json({ success: false, message: 'Missing authentication token' }, 401);
|
||||
}
|
||||
|
||||
// Validate user token
|
||||
setUserPocketBaseAuth(pbToken);
|
||||
try {
|
||||
await pb.collection(process.env.PB_AUTH_COLLECTION || 'Users').authRefresh();
|
||||
} catch (e) {
|
||||
console.error('auth_validation_failed', { message: (e as Error)?.message });
|
||||
return c.json({ success: false, message: 'Invalid token' }, 401);
|
||||
}
|
||||
|
||||
const collectionName = process.env.PB_COLLECTION || 'Job_Info_TestEnv';
|
||||
|
||||
// Clear all job caches
|
||||
await deleteCached(`jobs:*`);
|
||||
await invalidateSearchCache();
|
||||
|
||||
console.log('Cache cleared successfully');
|
||||
return c.json({ success: true, message: 'Cache cleared successfully' });
|
||||
} catch (err) {
|
||||
console.error('cache_clear_error', { message: (err as Error)?.message });
|
||||
return c.json({ success: false, message: 'Failed to clear cache' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = Number(process.env.PORT || 3025);
|
||||
|
||||
// Keep concise startup pointers
|
||||
console.log(`📊 Estimator Table: http://localhost:${PORT}/`);
|
||||
console.log(`⚙️ Settings: http://localhost:${PORT}/appsettings.html`);
|
||||
console.log(`🔍 Search API: http://localhost:${PORT}/api/jobs/search?q=<query>`);
|
||||
console.log(`💾 Redis caching enabled`);
|
||||
|
||||
export default {
|
||||
port: PORT,
|
||||
|
||||
Reference in New Issue
Block a user