nginx now checks every dashboard and API request with oauth2-proxy (auth_request), which signs users in with Microsoft Entra ID. Who may sign in is controlled in Entra: a single-tenant app registration with "Assignment required", so only assigned users/groups get through. - deploy/oauth2-proxy.cfg (entra-id provider, auth-only mode, secure cookie, trusted proxy = localhost) and deploy/oauth2-proxy.service - nginx: pages redirect to Microsoft sign-in and back to the same URL; API calls get a plain 401 (the page already asks to reload); /api/v1/ingest stays key-only for collectors; /oauth2/auth is internal; the signed-in user is passed to the API in headers nginx sets (so browsers can't spoof them); dashboard files are served with Cache-Control: no-cache so updates show up at once - GET /api/v1/me; the top bar shows the signed-in user and a Sign out that also signs out of Microsoft (otherwise SSO signs straight back in) - README: Entra and server setup steps Tested locally with oauth2-proxy 7.15.5 and nginx 1.31 running the deploy configs (TLS, self-signed): redirects to the tenant's sign-in with the return URL kept, 401s, ingest bypass, sign-out allow-list, cookie flags, security headers. The signed-in path was tested with a stand-in for oauth2-proxy's auth answer; a real Entra sign-in wasn't. Co-Authored-By: Claude Opus 5.5 <[email protected]>
65 lines
2.2 KiB
JavaScript
65 lines
2.2 KiB
JavaScript
import { CONFIG } from './config.js';
|
|
|
|
export class ApiError extends Error {
|
|
constructor(message, status = null) {
|
|
super(message);
|
|
this.name = 'ApiError';
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
async function request(path) {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), CONFIG.requestTimeoutMs);
|
|
try {
|
|
const res = await fetch(`${CONFIG.apiBaseUrl}${path}`, {
|
|
headers: { Accept: 'application/json' },
|
|
credentials: 'same-origin', // ready for cookie/session auth behind nginx
|
|
signal: controller.signal,
|
|
});
|
|
// 401 means the nginx/SSO session lapsed; a reload sends the user back through sign-in
|
|
if (res.status === 401) throw new ApiError('Your session has expired. Reload the page to sign in again', 401);
|
|
if (!res.ok) throw new ApiError(`Server returned HTTP ${res.status}`, res.status);
|
|
return await res.json();
|
|
} catch (err) {
|
|
if (err.name === 'AbortError') throw new ApiError('Request timed out');
|
|
if (err instanceof ApiError) throw err;
|
|
throw new ApiError(`Network error: ${err.message}`);
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
const httpApi = {
|
|
/** GET /jobs → { generated_at, jobs: Job[] } */
|
|
async listJobs() {
|
|
const data = await request('/jobs');
|
|
if (!Array.isArray(data?.jobs)) throw new ApiError('Unexpected response shape from /jobs');
|
|
return data;
|
|
},
|
|
/** GET /jobs/:id → JobDetail */
|
|
getJob(id) {
|
|
return request(`/jobs/${encodeURIComponent(id)}`);
|
|
},
|
|
/** GET /jobs/:id/runs?days=N → { job_id, since, runs: RunSummary[] } (newest first, no messages) */
|
|
listRuns(id, days) {
|
|
return request(`/jobs/${encodeURIComponent(id)}/runs?days=${encodeURIComponent(days)}`);
|
|
},
|
|
/** GET /runs/:id → Run (with message and details) */
|
|
getRun(id) {
|
|
return request(`/runs/${encodeURIComponent(id)}`);
|
|
},
|
|
/** GET /me → { email, name } of the Microsoft 365 user, both null without sign-in */
|
|
getMe() {
|
|
return request('/me');
|
|
},
|
|
};
|
|
|
|
// Mock is loaded lazily so it never ships in the production request path.
|
|
export async function getApi() {
|
|
if (CONFIG.useMock) {
|
|
const { mockApi } = await import('./mock.js');
|
|
return mockApi;
|
|
}
|
|
return httpApi;
|
|
} |