auth) addressing issues with detecting admins
Some checks failed
PR Checks / lint-test-and-docker-build (pull_request) Failing after 1m3s

This commit is contained in:
2026-02-22 00:04:50 -05:00
parent d44db1489c
commit a67b1db90a
16 changed files with 201 additions and 54 deletions

View File

@@ -6,6 +6,7 @@
"name": "trips", "name": "trips",
"dependencies": { "dependencies": {
"@auth/sveltekit": "^1.11.1", "@auth/sveltekit": "^1.11.1",
"argon2": "^0.41.1",
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.4", "@biomejs/biome": "^2.4.4",
@@ -153,6 +154,8 @@
"@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="], "@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="],
"@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.0", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ=="], "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.0", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ=="],
@@ -315,6 +318,8 @@
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"argon2": ["argon2@0.41.1", "", { "dependencies": { "@phc/format": "^1.0.0", "node-addon-api": "^8.1.0", "node-gyp-build": "^4.8.1" } }, "sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ=="],
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
@@ -509,6 +514,10 @@
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="],
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
"oauth4webapi": ["oauth4webapi@3.8.5", "", {}, "sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg=="], "oauth4webapi": ["oauth4webapi@3.8.5", "", {}, "sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg=="],
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],

View File

@@ -0,0 +1,81 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { setupTestDb } from '../../../tests/helpers.js';
import type { Database } from '../db/types.js';
import { upsertUserFromAuth } from '../users.js';
import { isAdminUser, requireAdmin } from './auth.js';
let database: Database;
const originalAdminUserIds = process.env.ADMIN_USER_IDS;
beforeEach(() => {
database = setupTestDb();
process.env.ADMIN_USER_IDS = '';
});
afterEach(() => {
process.env.ADMIN_USER_IDS = originalAdminUserIds;
database.close();
});
describe('isAdminUser', () => {
it('allows direct ID matches', () => {
process.env.ADMIN_USER_IDS = 'oidc-sub-123';
expect(isAdminUser('oidc-sub-123')).toBe(true);
});
it('allows username matches for known users', () => {
upsertUserFromAuth({
id: 'oidc-sub-123',
username: 'shaun',
fullName: 'Shaun Campbell',
email: 'shaun@example.com'
});
process.env.ADMIN_USER_IDS = 'shaun';
expect(isAdminUser('oidc-sub-123')).toBe(true);
});
it('allows email matches for known users', () => {
upsertUserFromAuth({
id: 'oidc-sub-123',
username: 'shaun',
fullName: 'Shaun Campbell',
email: 'shaun@example.com'
});
process.env.ADMIN_USER_IDS = 'shaun@example.com';
expect(isAdminUser('oidc-sub-123')).toBe(true);
});
it('allows email local-part matches from session user', () => {
process.env.ADMIN_USER_IDS = 'shaun';
expect(isAdminUser({ id: 'oidc-sub-123', email: 'shaun@example.com' })).toBe(true);
});
});
describe('requireAdmin', () => {
it('throws when not authenticated', () => {
process.env.ADMIN_USER_IDS = 'shaun';
expect(() => requireAdmin(undefined)).toThrow('Not authenticated');
});
it('throws when user is not admin', () => {
process.env.ADMIN_USER_IDS = 'shaun';
expect(() => requireAdmin('someone-else')).toThrow('Admin access required');
});
it('does not throw when username match grants admin access', () => {
upsertUserFromAuth({
id: 'oidc-sub-123',
username: 'shaun',
fullName: 'Shaun Campbell'
});
process.env.ADMIN_USER_IDS = 'shaun';
expect(() => requireAdmin('oidc-sub-123')).not.toThrow();
});
it('does not throw when session user email local-part matches', () => {
process.env.ADMIN_USER_IDS = 'shaun';
expect(() =>
requireAdmin({ id: 'oidc-sub-123', email: 'shaun@example.com', name: 'Shaun Campbell' })
).not.toThrow();
});
});

View File

@@ -1,12 +1,73 @@
import { env } from '$env/dynamic/private'; import { env } from '$env/dynamic/private';
import { db } from '$lib/server/db/index.js';
export function requireAdmin(userId: string | undefined): void { const getAdminIdentifiers = (): string[] =>
if (!userId) throw new Error('Not authenticated'); (process.env.ADMIN_USER_IDS ?? env.ADMIN_USER_IDS ?? '')
const adminIds = (env.ADMIN_USER_IDS ?? '')
.split(',') .split(',')
.map((s) => s.trim()) .map((s) => s.trim())
.filter(Boolean); .filter(Boolean);
if (adminIds.length === 0 || !adminIds.includes(userId)) {
type AdminPrincipal =
| string
| {
id?: string | null;
name?: string | null;
email?: string | null;
}
| undefined;
const normalize = (value: string): string => value.trim().toLowerCase();
const getPrincipalValues = (principal: AdminPrincipal): { userId?: string; values: string[] } => {
if (!principal) return { values: [] };
if (typeof principal === 'string') {
const value = principal.trim();
return value ? { userId: value, values: [value] } : { values: [] };
}
const values = [principal.id, principal.name, principal.email]
.filter((value): value is string => Boolean(value?.trim()))
.map((value) => value.trim());
const localPart = principal.email?.split('@')[0]?.trim();
if (localPart) values.push(localPart);
const userId = principal.id?.trim();
return { userId, values };
};
export function isAdminUser(principal: AdminPrincipal): boolean {
const { userId, values } = getPrincipalValues(principal);
if (values.length === 0) return false;
const adminIds = getAdminIdentifiers();
if (adminIds.length === 0) return false;
const normalizedAdminIds = new Set(adminIds.map(normalize));
for (const value of values) {
if (adminIds.includes(value) || normalizedAdminIds.has(normalize(value))) return true;
}
if (!userId) return false;
const user = db.get<{ username: string; email: string | null }>(
'SELECT username, email FROM users WHERE id = ?',
[userId]
);
if (!user) return false;
const dbValues = [user.username, user.email, user.email?.split('@')[0]]
.filter((value): value is string => Boolean(value?.trim()))
.map((value) => value.trim());
for (const value of dbValues) {
if (adminIds.includes(value) || normalizedAdminIds.has(normalize(value))) return true;
}
return false;
}
export function requireAdmin(principal: AdminPrincipal): void {
if (!principal) throw new Error('Not authenticated');
if (!isAdminUser(principal)) {
throw new Error('Admin access required'); throw new Error('Admin access required');
} }
} }

View File

@@ -1,6 +1,6 @@
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import { base } from '$app/paths'; import { base } from '$app/paths';
import { env } from '$env/dynamic/private'; import { isAdminUser } from '$lib/server/admin/auth.js';
import type { LayoutServerLoad } from './$types'; import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async (event) => { export const load: LayoutServerLoad = async (event) => {
@@ -9,11 +9,7 @@ export const load: LayoutServerLoad = async (event) => {
redirect(303, `${base}/login`); redirect(303, `${base}/login`);
} }
const adminIds = (env.ADMIN_USER_IDS ?? '') const isAdmin = isAdminUser(session.user);
.split(',')
.map((s) => s.trim())
.filter(Boolean);
const isAdmin = adminIds.length > 0 && session.user.id && adminIds.includes(session.user.id);
return { session, isAdmin }; return { session, isAdmin };
}; };

View File

@@ -4,13 +4,13 @@ import { requireAdmin } from '$lib/server/admin/auth.js';
import * as data from '$lib/server/admin/data.js'; import * as data from '$lib/server/admin/data.js';
export const GET: RequestHandler = async (event) => { export const GET: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const q = event.url.searchParams.get('q') ?? undefined; const q = event.url.searchParams.get('q') ?? undefined;
return json(data.listAirlines(q)); return json(data.listAirlines(q));
}; };
export const POST: RequestHandler = async (event) => { export const POST: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const body = await event.request.json(); const body = await event.request.json();
const { name, country, country_code, iata_code, icao_code } = body as { const { name, country, country_code, iata_code, icao_code } = body as {
name?: string; name?: string;
@@ -32,7 +32,7 @@ export const POST: RequestHandler = async (event) => {
}; };
export const PATCH: RequestHandler = async (event) => { export const PATCH: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const body = await event.request.json(); const body = await event.request.json();
const { id, name, country, country_code, iata_code, icao_code } = body as { const { id, name, country, country_code, iata_code, icao_code } = body as {
id?: number; id?: number;
@@ -55,7 +55,7 @@ export const PATCH: RequestHandler = async (event) => {
}; };
export const DELETE: RequestHandler = async (event) => { export const DELETE: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const id = parseInt(event.url.searchParams.get('id') ?? ''); const id = parseInt(event.url.searchParams.get('id') ?? '');
if (isNaN(id)) return json({ error: 'id required' }, { status: 400 }); if (isNaN(id)) return json({ error: 'id required' }, { status: 400 });
data.deleteAirline(id); data.deleteAirline(id);

View File

@@ -4,13 +4,13 @@ import { requireAdmin } from '$lib/server/admin/auth.js';
import * as data from '$lib/server/admin/data.js'; import * as data from '$lib/server/admin/data.js';
export const GET: RequestHandler = async (event) => { export const GET: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const q = event.url.searchParams.get('q') ?? undefined; const q = event.url.searchParams.get('q') ?? undefined;
return json(data.listAirports(q)); return json(data.listAirports(q));
}; };
export const POST: RequestHandler = async (event) => { export const POST: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const body = await event.request.json(); const body = await event.request.json();
const { name, country, country_code, iata_code, icao_code, city, latitude, longitude, timezone } = const { name, country, country_code, iata_code, icao_code, city, latitude, longitude, timezone } =
body as { body as {
@@ -43,7 +43,7 @@ export const POST: RequestHandler = async (event) => {
}; };
export const PATCH: RequestHandler = async (event) => { export const PATCH: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const body = await event.request.json(); const body = await event.request.json();
const { const {
id, id,
@@ -87,7 +87,7 @@ export const PATCH: RequestHandler = async (event) => {
}; };
export const DELETE: RequestHandler = async (event) => { export const DELETE: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const id = parseInt(event.url.searchParams.get('id') ?? ''); const id = parseInt(event.url.searchParams.get('id') ?? '');
if (isNaN(id)) return json({ error: 'id required' }, { status: 400 }); if (isNaN(id)) return json({ error: 'id required' }, { status: 400 });
data.deleteAirport(id); data.deleteAirport(id);

View File

@@ -4,14 +4,14 @@ import { requireAdmin } from '$lib/server/admin/auth.js';
import * as data from '$lib/server/admin/data.js'; import * as data from '$lib/server/admin/data.js';
export const GET: RequestHandler = async (event) => { export const GET: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const q = event.url.searchParams.get('q') ?? undefined; const q = event.url.searchParams.get('q') ?? undefined;
const countryCode = event.url.searchParams.get('country_code') ?? undefined; const countryCode = event.url.searchParams.get('country_code') ?? undefined;
return json(data.listCities(q, countryCode)); return json(data.listCities(q, countryCode));
}; };
export const POST: RequestHandler = async (event) => { export const POST: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const body = await event.request.json(); const body = await event.request.json();
const { name, country, country_code, population } = body as { const { name, country, country_code, population } = body as {
name?: string; name?: string;
@@ -26,7 +26,7 @@ export const POST: RequestHandler = async (event) => {
}; };
export const PATCH: RequestHandler = async (event) => { export const PATCH: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const body = await event.request.json(); const body = await event.request.json();
const { id, name, country, country_code, population } = body as { const { id, name, country, country_code, population } = body as {
id?: number; id?: number;
@@ -43,7 +43,7 @@ export const PATCH: RequestHandler = async (event) => {
}; };
export const DELETE: RequestHandler = async (event) => { export const DELETE: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const id = parseInt(event.url.searchParams.get('id') ?? ''); const id = parseInt(event.url.searchParams.get('id') ?? '');
if (isNaN(id)) return json({ error: 'id required' }, { status: 400 }); if (isNaN(id)) return json({ error: 'id required' }, { status: 400 });
data.deleteCity(id); data.deleteCity(id);

View File

@@ -4,13 +4,13 @@ import { requireAdmin } from '$lib/server/admin/auth.js';
import * as data from '$lib/server/admin/data.js'; import * as data from '$lib/server/admin/data.js';
export const GET: RequestHandler = async (event) => { export const GET: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const q = event.url.searchParams.get('q') ?? undefined; const q = event.url.searchParams.get('q') ?? undefined;
return json(data.listCountries(q)); return json(data.listCountries(q));
}; };
export const POST: RequestHandler = async (event) => { export const POST: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const body = await event.request.json(); const body = await event.request.json();
const { name, country_code } = body as { name?: string; country_code?: string }; const { name, country_code } = body as { name?: string; country_code?: string };
if (!name?.trim() || !country_code?.trim()) { if (!name?.trim() || !country_code?.trim()) {
@@ -20,7 +20,7 @@ export const POST: RequestHandler = async (event) => {
}; };
export const PATCH: RequestHandler = async (event) => { export const PATCH: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const body = await event.request.json(); const body = await event.request.json();
const { id, name, country_code } = body as { id?: number; name?: string; country_code?: string }; const { id, name, country_code } = body as { id?: number; name?: string; country_code?: string };
if (id == null || !name?.trim() || !country_code?.trim()) { if (id == null || !name?.trim() || !country_code?.trim()) {
@@ -31,7 +31,7 @@ export const PATCH: RequestHandler = async (event) => {
}; };
export const DELETE: RequestHandler = async (event) => { export const DELETE: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const id = parseInt(event.url.searchParams.get('id') ?? ''); const id = parseInt(event.url.searchParams.get('id') ?? '');
if (isNaN(id)) return json({ error: 'id required' }, { status: 400 }); if (isNaN(id)) return json({ error: 'id required' }, { status: 400 });
data.deleteCountry(id); data.deleteCountry(id);

View File

@@ -4,14 +4,14 @@ import { requireAdmin } from '$lib/server/admin/auth.js';
import * as data from '$lib/server/admin/data.js'; import * as data from '$lib/server/admin/data.js';
export const GET: RequestHandler = async (event) => { export const GET: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const tourId = parseInt(event.url.searchParams.get('operator_tour_id') ?? ''); const tourId = parseInt(event.url.searchParams.get('operator_tour_id') ?? '');
if (isNaN(tourId)) return json({ error: 'operator_tour_id required' }, { status: 400 }); if (isNaN(tourId)) return json({ error: 'operator_tour_id required' }, { status: 400 });
return json(data.listDaysForOperatorTour(tourId)); return json(data.listDaysForOperatorTour(tourId));
}; };
export const POST: RequestHandler = async (event) => { export const POST: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const body = await event.request.json(); const body = await event.request.json();
const { operator_tour_id, title, notes } = body as { const { operator_tour_id, title, notes } = body as {
operator_tour_id?: number; operator_tour_id?: number;
@@ -25,7 +25,7 @@ export const POST: RequestHandler = async (event) => {
}; };
export const PATCH: RequestHandler = async (event) => { export const PATCH: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const body = await event.request.json(); const body = await event.request.json();
const { id, title, notes } = body as { id?: number; title?: string; notes?: string }; const { id, title, notes } = body as { id?: number; title?: string; notes?: string };
if (id == null) return json({ error: 'id is required' }, { status: 400 }); if (id == null) return json({ error: 'id is required' }, { status: 400 });
@@ -34,7 +34,7 @@ export const PATCH: RequestHandler = async (event) => {
}; };
export const DELETE: RequestHandler = async (event) => { export const DELETE: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const id = parseInt(event.url.searchParams.get('id') ?? ''); const id = parseInt(event.url.searchParams.get('id') ?? '');
if (isNaN(id)) return json({ error: 'id required' }, { status: 400 }); if (isNaN(id)) return json({ error: 'id required' }, { status: 400 });
data.deleteOperatorTourDay(id); data.deleteOperatorTourDay(id);

View File

@@ -4,14 +4,14 @@ import { requireAdmin } from '$lib/server/admin/auth.js';
import * as data from '$lib/server/admin/data.js'; import * as data from '$lib/server/admin/data.js';
export const GET: RequestHandler = async (event) => { export const GET: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const operatorId = parseInt(event.url.searchParams.get('operator_id') ?? ''); const operatorId = parseInt(event.url.searchParams.get('operator_id') ?? '');
if (isNaN(operatorId)) return json({ error: 'operator_id required' }, { status: 400 }); if (isNaN(operatorId)) return json({ error: 'operator_id required' }, { status: 400 });
return json(data.listToursForOperator(operatorId)); return json(data.listToursForOperator(operatorId));
}; };
export const POST: RequestHandler = async (event) => { export const POST: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const body = await event.request.json(); const body = await event.request.json();
const { operator_id, name } = body as { operator_id?: number; name?: string }; const { operator_id, name } = body as { operator_id?: number; name?: string };
if (operator_id == null || !name?.trim()) { if (operator_id == null || !name?.trim()) {
@@ -21,7 +21,7 @@ export const POST: RequestHandler = async (event) => {
}; };
export const PATCH: RequestHandler = async (event) => { export const PATCH: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const body = await event.request.json(); const body = await event.request.json();
const { id, name } = body as { id?: number; name?: string }; const { id, name } = body as { id?: number; name?: string };
if (id == null || !name?.trim()) { if (id == null || !name?.trim()) {
@@ -32,7 +32,7 @@ export const PATCH: RequestHandler = async (event) => {
}; };
export const DELETE: RequestHandler = async (event) => { export const DELETE: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const id = parseInt(event.url.searchParams.get('id') ?? ''); const id = parseInt(event.url.searchParams.get('id') ?? '');
if (isNaN(id)) return json({ error: 'id required' }, { status: 400 }); if (isNaN(id)) return json({ error: 'id required' }, { status: 400 });
data.deleteOperatorTour(id); data.deleteOperatorTour(id);

View File

@@ -5,7 +5,7 @@ import { listTourOperators } from '$lib/server/admin/data.js';
import { getProviderForOperator } from '$lib/server/admin/providers/index.js'; import { getProviderForOperator } from '$lib/server/admin/providers/index.js';
export const GET: RequestHandler = async (event) => { export const GET: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const operatorId = parseInt(event.url.searchParams.get('operator_id') ?? ''); const operatorId = parseInt(event.url.searchParams.get('operator_id') ?? '');
if (isNaN(operatorId)) return json({ error: 'operator_id required' }, { status: 400 }); if (isNaN(operatorId)) return json({ error: 'operator_id required' }, { status: 400 });

View File

@@ -4,13 +4,13 @@ import { requireAdmin } from '$lib/server/admin/auth.js';
import * as data from '$lib/server/admin/data.js'; import * as data from '$lib/server/admin/data.js';
export const GET: RequestHandler = async (event) => { export const GET: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const q = event.url.searchParams.get('q') ?? undefined; const q = event.url.searchParams.get('q') ?? undefined;
return json(data.listTourOperators(q)); return json(data.listTourOperators(q));
}; };
export const POST: RequestHandler = async (event) => { export const POST: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const body = await event.request.json(); const body = await event.request.json();
const { name, website, highlight_color } = body as { const { name, website, highlight_color } = body as {
name?: string; name?: string;
@@ -24,7 +24,7 @@ export const POST: RequestHandler = async (event) => {
}; };
export const PATCH: RequestHandler = async (event) => { export const PATCH: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const body = await event.request.json(); const body = await event.request.json();
const { id, name, website, highlight_color } = body as { const { id, name, website, highlight_color } = body as {
id?: number; id?: number;
@@ -40,7 +40,7 @@ export const PATCH: RequestHandler = async (event) => {
}; };
export const DELETE: RequestHandler = async (event) => { export const DELETE: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const id = parseInt(event.url.searchParams.get('id') ?? ''); const id = parseInt(event.url.searchParams.get('id') ?? '');
if (isNaN(id)) return json({ error: 'id required' }, { status: 400 }); if (isNaN(id)) return json({ error: 'id required' }, { status: 400 });
data.deleteTourOperator(id); data.deleteTourOperator(id);

View File

@@ -4,12 +4,12 @@ import { requireAdmin } from '$lib/server/admin/auth.js';
import { deleteUser, listUsers, updateUser } from '$lib/server/users.js'; import { deleteUser, listUsers, updateUser } from '$lib/server/users.js';
export const GET: RequestHandler = async (event) => { export const GET: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
return json(listUsers()); return json(listUsers());
}; };
export const PATCH: RequestHandler = async (event) => { export const PATCH: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const body = await event.request.json(); const body = await event.request.json();
const { id, username, full_name, email } = body as { const { id, username, full_name, email } = body as {
id?: string; id?: string;
@@ -25,7 +25,7 @@ export const PATCH: RequestHandler = async (event) => {
}; };
export const DELETE: RequestHandler = async (event) => { export const DELETE: RequestHandler = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const id = event.url.searchParams.get('id')?.trim(); const id = event.url.searchParams.get('id')?.trim();
if (!id) return json({ error: 'id required' }, { status: 400 }); if (!id) return json({ error: 'id required' }, { status: 400 });
deleteUser(id); deleteUser(id);

View File

@@ -5,7 +5,7 @@ import { getProviderForOperator } from '$lib/server/admin/providers/index.js';
import type { PageServerLoad, Actions } from './$types'; import type { PageServerLoad, Actions } from './$types';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const operatorId = parseInt(event.params.operatorId); const operatorId = parseInt(event.params.operatorId);
if (isNaN(operatorId)) error(404, 'Not found'); if (isNaN(operatorId)) error(404, 'Not found');
@@ -27,7 +27,7 @@ export const load: PageServerLoad = async (event) => {
export const actions: Actions = { export const actions: Actions = {
addTour: async (event) => { addTour: async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const operatorId = parseInt(event.params.operatorId); const operatorId = parseInt(event.params.operatorId);
if (isNaN(operatorId)) return fail(400, { error: 'Invalid operator ID' }); if (isNaN(operatorId)) return fail(400, { error: 'Invalid operator ID' });
@@ -45,7 +45,7 @@ export const actions: Actions = {
}, },
editTour: async (event) => { editTour: async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const formData = await event.request.formData(); const formData = await event.request.formData();
const id = parseInt(formData.get('id') as string); const id = parseInt(formData.get('id') as string);
@@ -63,7 +63,7 @@ export const actions: Actions = {
}, },
deleteTour: async (event) => { deleteTour: async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const formData = await event.request.formData(); const formData = await event.request.formData();
const id = parseInt(formData.get('id') as string); const id = parseInt(formData.get('id') as string);
@@ -78,7 +78,7 @@ export const actions: Actions = {
}, },
importTour: async (event) => { importTour: async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const operatorId = parseInt(event.params.operatorId); const operatorId = parseInt(event.params.operatorId);
if (isNaN(operatorId)) return fail(400, { error: 'Invalid operator ID' }); if (isNaN(operatorId)) return fail(400, { error: 'Invalid operator ID' });

View File

@@ -4,7 +4,7 @@ import * as data from '$lib/server/admin/data.js';
import type { PageServerLoad, Actions } from './$types'; import type { PageServerLoad, Actions } from './$types';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const operatorId = parseInt(event.params.operatorId); const operatorId = parseInt(event.params.operatorId);
const tourId = parseInt(event.params.tourId); const tourId = parseInt(event.params.tourId);
@@ -27,7 +27,7 @@ export const load: PageServerLoad = async (event) => {
export const actions: Actions = { export const actions: Actions = {
addDay: async (event) => { addDay: async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const tourId = parseInt(event.params.tourId); const tourId = parseInt(event.params.tourId);
if (isNaN(tourId)) return fail(400, { error: 'Invalid tour ID' }); if (isNaN(tourId)) return fail(400, { error: 'Invalid tour ID' });
@@ -45,7 +45,7 @@ export const actions: Actions = {
}, },
editDay: async (event) => { editDay: async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const formData = await event.request.formData(); const formData = await event.request.formData();
const id = parseInt(formData.get('id') as string); const id = parseInt(formData.get('id') as string);
@@ -63,7 +63,7 @@ export const actions: Actions = {
}, },
deleteDay: async (event) => { deleteDay: async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const formData = await event.request.formData(); const formData = await event.request.formData();
const id = parseInt(formData.get('id') as string); const id = parseInt(formData.get('id') as string);
@@ -78,7 +78,7 @@ export const actions: Actions = {
}, },
addDayPlan: async (event) => { addDayPlan: async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const formData = await event.request.formData(); const formData = await event.request.formData();
const dayId = parseInt(formData.get('dayId') as string); const dayId = parseInt(formData.get('dayId') as string);
@@ -247,7 +247,7 @@ export const actions: Actions = {
}, },
editDayPlan: async (event) => { editDayPlan: async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const formData = await event.request.formData(); const formData = await event.request.formData();
const id = parseInt(formData.get('id') as string); const id = parseInt(formData.get('id') as string);
@@ -310,7 +310,7 @@ export const actions: Actions = {
}, },
deleteDayPlan: async (event) => { deleteDayPlan: async (event) => {
requireAdmin((await event.locals.auth())?.user?.id); requireAdmin((await event.locals.auth())?.user);
const formData = await event.request.formData(); const formData = await event.request.formData();
const id = parseInt(formData.get('id') as string); const id = parseInt(formData.get('id') as string);