umami tracking
This commit is contained in:
parent
072feaf373
commit
e56aa1aa03
@ -19,6 +19,7 @@ import type {
|
|||||||
VisaSponsorStatusResponse,
|
VisaSponsorStatusResponse,
|
||||||
VisaSponsor,
|
VisaSponsor,
|
||||||
} from '../../shared/types';
|
} from '../../shared/types';
|
||||||
|
import { trackEvent } from '../lib/analytics';
|
||||||
|
|
||||||
const API_BASE = '/api';
|
const API_BASE = '/api';
|
||||||
|
|
||||||
@ -116,6 +117,12 @@ export async function searchUkVisaJobs(input: {
|
|||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
}): Promise<UkVisaJobsSearchResponse> {
|
}): Promise<UkVisaJobsSearchResponse> {
|
||||||
|
if (input.searchTerm?.trim()) {
|
||||||
|
trackEvent('ukvisajobs_search', {
|
||||||
|
searchTerm: input.searchTerm.trim(),
|
||||||
|
page: input.page ?? 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
return fetchApi<UkVisaJobsSearchResponse>('/ukvisajobs/search', {
|
return fetchApi<UkVisaJobsSearchResponse>('/ukvisajobs/search', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(input),
|
body: JSON.stringify(input),
|
||||||
@ -202,6 +209,13 @@ export async function searchVisaSponsors(input: {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
minScore?: number;
|
minScore?: number;
|
||||||
}): Promise<VisaSponsorSearchResponse> {
|
}): Promise<VisaSponsorSearchResponse> {
|
||||||
|
if (input.query?.trim()) {
|
||||||
|
trackEvent('visa_sponsor_search', {
|
||||||
|
query: input.query.trim(),
|
||||||
|
limit: input.limit,
|
||||||
|
minScore: input.minScore,
|
||||||
|
});
|
||||||
|
}
|
||||||
return fetchApi<VisaSponsorSearchResponse>('/visa-sponsors/search', {
|
return fetchApi<VisaSponsorSearchResponse>('/visa-sponsors/search', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(input),
|
body: JSON.stringify(input),
|
||||||
|
|||||||
14
orchestrator/src/client/lib/analytics.ts
Normal file
14
orchestrator/src/client/lib/analytics.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
type UmamiTracker = {
|
||||||
|
track: (event: string, data?: Record<string, unknown>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
umami?: UmamiTracker;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trackEvent(event: string, data?: Record<string, unknown>) {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
window.umami?.track(event, data);
|
||||||
|
}
|
||||||
@ -6,17 +6,18 @@ import express from 'express';
|
|||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import { readFile } from 'fs/promises';
|
||||||
import { apiRouter } from './api/index.js';
|
import { apiRouter } from './api/index.js';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
function buildBasicAuthMiddleware() {
|
function createBasicAuthGuard() {
|
||||||
const BASIC_AUTH_USER = process.env.BASIC_AUTH_USER || '';
|
const BASIC_AUTH_USER = process.env.BASIC_AUTH_USER || '';
|
||||||
const BASIC_AUTH_PASSWORD = process.env.BASIC_AUTH_PASSWORD || '';
|
const BASIC_AUTH_PASSWORD = process.env.BASIC_AUTH_PASSWORD || '';
|
||||||
const basicAuthEnabled = BASIC_AUTH_USER.length > 0 && BASIC_AUTH_PASSWORD.length > 0;
|
const basicAuthEnabled = BASIC_AUTH_USER.length > 0 && BASIC_AUTH_PASSWORD.length > 0;
|
||||||
|
|
||||||
function isAuthorized(req: express.Request): boolean {
|
function isAuthorized(req: express.Request): boolean {
|
||||||
if (!basicAuthEnabled) return true;
|
if (!basicAuthEnabled) return false;
|
||||||
const authHeader = req.headers.authorization || '';
|
const authHeader = req.headers.authorization || '';
|
||||||
if (!authHeader.startsWith('Basic ')) return false;
|
if (!authHeader.startsWith('Basic ')) return false;
|
||||||
const encoded = authHeader.slice('Basic '.length).trim();
|
const encoded = authHeader.slice('Basic '.length).trim();
|
||||||
@ -46,16 +47,33 @@ function buildBasicAuthMiddleware() {
|
|||||||
return !['GET', 'HEAD', 'OPTIONS'].includes(method.toUpperCase());
|
return !['GET', 'HEAD', 'OPTIONS'].includes(method.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
const middleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
if (!basicAuthEnabled || !requiresAuth(req.method, req.path)) return next();
|
if (!basicAuthEnabled || !requiresAuth(req.method, req.path)) return next();
|
||||||
if (isAuthorized(req)) return next();
|
if (isAuthorized(req)) return next();
|
||||||
res.setHeader('WWW-Authenticate', 'Basic realm="Job Ops"');
|
res.setHeader('WWW-Authenticate', 'Basic realm="Job Ops"');
|
||||||
res.status(401).send('Authentication required');
|
res.status(401).send('Authentication required');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
middleware,
|
||||||
|
isAuthorized,
|
||||||
|
basicAuthEnabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectUmami(html: string): string {
|
||||||
|
const snippet =
|
||||||
|
'<script defer src="https://umami.dakheera47.com/script.js" data-website-id="0dc42ed1-87c3-4ac0-9409-5a9b9588fe66"></script>';
|
||||||
|
if (html.includes(snippet)) return html;
|
||||||
|
if (html.includes('</head>')) {
|
||||||
|
return html.replace('</head>', `${snippet}\n</head>`);
|
||||||
|
}
|
||||||
|
return `${html}\n${snippet}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createApp() {
|
export function createApp() {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
const authGuard = createBasicAuthGuard();
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@ -71,7 +89,7 @@ export function createApp() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Optional Basic Auth for write access (read-only by default)
|
// Optional Basic Auth for write access (read-only by default)
|
||||||
app.use(buildBasicAuthMiddleware());
|
app.use(authGuard.middleware);
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
app.use('/api', apiRouter);
|
app.use('/api', apiRouter);
|
||||||
@ -93,8 +111,20 @@ export function createApp() {
|
|||||||
app.use(express.static(clientDir));
|
app.use(express.static(clientDir));
|
||||||
|
|
||||||
// SPA fallback
|
// SPA fallback
|
||||||
app.get('*', (_req, res) => {
|
const indexPath = join(clientDir, 'index.html');
|
||||||
res.sendFile(join(clientDir, 'index.html'));
|
let cachedIndexHtml: string | null = null;
|
||||||
|
app.get('*', async (req, res) => {
|
||||||
|
if (!req.accepts('html')) {
|
||||||
|
res.status(404).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!cachedIndexHtml) {
|
||||||
|
cachedIndexHtml = await readFile(indexPath, 'utf-8');
|
||||||
|
}
|
||||||
|
const isAuthenticated = authGuard.basicAuthEnabled && authGuard.isAuthorized(req);
|
||||||
|
const html = isAuthenticated ? cachedIndexHtml : injectUmami(cachedIndexHtml);
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
res.send(html);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user