first ai implementation, oidc done
This commit is contained in:
parent
1573557164
commit
32269b489c
1
.astro/types.d.ts
vendored
1
.astro/types.d.ts
vendored
@ -1 +1,2 @@
|
||||
/// <reference types="astro/client" />
|
||||
/// <reference path="content.d.ts" />
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,6 +4,8 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
package-lock.json
|
||||
|
||||
# Build output
|
||||
_site/
|
||||
dist/
|
||||
|
@ -1,9 +1,13 @@
|
||||
// astro.config.mjs - Static deployment configuration
|
||||
// astro.config.mjs - SSR configuration for authentication
|
||||
import { defineConfig } from 'astro/config';
|
||||
import node from '@astrojs/node';
|
||||
|
||||
export default defineConfig({
|
||||
// Static site generation - no adapter needed
|
||||
output: 'static',
|
||||
// Server-side rendering for authentication and API routes
|
||||
output: 'server',
|
||||
adapter: node({
|
||||
mode: 'standalone'
|
||||
}),
|
||||
|
||||
// Build configuration
|
||||
build: {
|
||||
@ -14,10 +18,5 @@ export default defineConfig({
|
||||
server: {
|
||||
port: 4321,
|
||||
host: true
|
||||
},
|
||||
|
||||
// Ensure all pages are pre-rendered
|
||||
experimental: {
|
||||
prerender: true
|
||||
}
|
||||
});
|
82
package-lock.json
generated
82
package-lock.json
generated
@ -8,17 +8,19 @@
|
||||
"name": "dfir-tools-hub",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^9.3.0",
|
||||
"astro": "^5.3.0",
|
||||
"cookie": "^0.6.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"jose": "^5.2.0",
|
||||
"js-yaml": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/js-yaml": "^4.0.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@astrojs/node": "^9.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@astrojs/compiler": {
|
||||
@ -67,7 +69,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@astrojs/node/-/node-9.3.0.tgz",
|
||||
"integrity": "sha512-IV8NzGStHAsKBz1ljxxD8PBhBfnw/BEx/PZfsncTNXg9D4kQtZbSy+Ak0LvDs+rPmK0VeXLNn0HAdWuHCVg8cw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@astrojs/internal-helpers": "0.6.1",
|
||||
"send": "^1.2.0",
|
||||
@ -1374,6 +1375,13 @@
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||
@ -1679,6 +1687,15 @@
|
||||
"sharp": "^0.33.3"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@ -1962,12 +1979,12 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-es": {
|
||||
@ -2060,7 +2077,6 @@
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@ -2142,6 +2158,18 @@
|
||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dset": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz",
|
||||
@ -2155,8 +2183,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "10.4.0",
|
||||
@ -2169,7 +2196,6 @@
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@ -2237,8 +2263,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "5.0.0",
|
||||
@ -2266,7 +2291,6 @@
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -2344,7 +2368,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@ -2612,7 +2635,6 @@
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
@ -2629,7 +2651,6 @@
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@ -2648,8 +2669,7 @@
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/iron-webcrypto": {
|
||||
"version": "1.2.1",
|
||||
@ -2736,6 +2756,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
|
||||
"integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
@ -3602,7 +3631,6 @@
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -3612,7 +3640,6 @@
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
|
||||
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
@ -3738,7 +3765,6 @@
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
@ -3946,7 +3972,6 @@
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -4240,7 +4265,6 @@
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
||||
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.3.5",
|
||||
"encodeurl": "^2.0.0",
|
||||
@ -4262,15 +4286,13 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
|
||||
"integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.33.5",
|
||||
@ -4403,7 +4425,6 @@
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@ -4507,7 +4528,6 @@
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
|
12
package.json
12
package.json
@ -14,13 +14,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^5.3.0",
|
||||
"js-yaml": "^4.1.0"
|
||||
"@astrojs/node": "^9.3.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jose": "^5.2.0",
|
||||
"cookie": "^0.6.0",
|
||||
"dotenv": "^16.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.9"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@astrojs/node": "^9.3.0"
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/cookie": "^0.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
293
src/pages/api/ai/query.ts
Normal file
293
src/pages/api/ai/query.ts
Normal file
@ -0,0 +1,293 @@
|
||||
// src/pages/api/ai/query.ts
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
|
||||
import { promises as fs } from 'fs';
|
||||
import { load } from 'js-yaml';
|
||||
import path from 'path';
|
||||
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
|
||||
function getEnv(key: string): string {
|
||||
const value = process.env[key];
|
||||
if (!value) {
|
||||
throw new Error(`Missing environment variable: ${key}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const AI_MODEL = getEnv('AI_MODEL');
|
||||
// Rate limiting store (in production, use Redis)
|
||||
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
|
||||
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
|
||||
const RATE_LIMIT_MAX = 10; // 10 requests per minute per user
|
||||
|
||||
// Input validation and sanitization
|
||||
function sanitizeInput(input: string): string {
|
||||
// Remove potential prompt injection patterns
|
||||
const dangerous = [
|
||||
/ignore\s+previous\s+instructions?/gi,
|
||||
/new\s+instructions?:/gi,
|
||||
/system\s*:/gi,
|
||||
/assistant\s*:/gi,
|
||||
/human\s*:/gi,
|
||||
/<\s*\/?system\s*>/gi,
|
||||
/```\s*system/gi,
|
||||
];
|
||||
|
||||
let sanitized = input.trim();
|
||||
dangerous.forEach(pattern => {
|
||||
sanitized = sanitized.replace(pattern, '[FILTERED]');
|
||||
});
|
||||
|
||||
// Limit length
|
||||
return sanitized.slice(0, 2000);
|
||||
}
|
||||
|
||||
// Strip markdown code blocks from AI response
|
||||
function stripMarkdownJson(content: string): string {
|
||||
// Remove ```json and ``` wrappers
|
||||
return content
|
||||
.replace(/^```json\s*/i, '')
|
||||
.replace(/\s*```\s*$/, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
// Rate limiting check
|
||||
function checkRateLimit(userId: string): boolean {
|
||||
const now = Date.now();
|
||||
const userLimit = rateLimitStore.get(userId);
|
||||
|
||||
if (!userLimit || now > userLimit.resetTime) {
|
||||
rateLimitStore.set(userId, { count: 1, resetTime: now + RATE_LIMIT_WINDOW });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (userLimit.count >= RATE_LIMIT_MAX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
userLimit.count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load tools database
|
||||
async function loadToolsDatabase() {
|
||||
try {
|
||||
const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml');
|
||||
const yamlContent = await fs.readFile(yamlPath, 'utf8');
|
||||
return load(yamlContent) as any;
|
||||
} catch (error) {
|
||||
console.error('Failed to load tools database:', error);
|
||||
throw new Error('Database unavailable');
|
||||
}
|
||||
}
|
||||
|
||||
// Create system prompt
|
||||
function createSystemPrompt(toolsData: any): string {
|
||||
const toolsList = toolsData.tools.map((tool: any) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
domains: tool.domains,
|
||||
phases: tool.phases,
|
||||
platforms: tool.platforms,
|
||||
skillLevel: tool.skillLevel,
|
||||
license: tool.license,
|
||||
tags: tool.tags,
|
||||
projectUrl: tool.projectUrl ? 'self-hosted' : 'external'
|
||||
}));
|
||||
|
||||
// Dynamically build phases list from configuration
|
||||
const phasesDescription = toolsData.phases.map((phase: any) =>
|
||||
`- ${phase.id}: ${phase.name}`
|
||||
).join('\n');
|
||||
|
||||
// Dynamically build domains list from configuration
|
||||
const domainsDescription = toolsData.domains.map((domain: any) =>
|
||||
`- ${domain.id}: ${domain.name}`
|
||||
).join('\n');
|
||||
|
||||
return `Du bist ein DFIR (Digital Forensics and Incident Response) Experte, der Ermittlern bei der Toolauswahl hilft.
|
||||
|
||||
VERFÜGBARE TOOLS DATABASE:
|
||||
${JSON.stringify(toolsList, null, 2)}
|
||||
|
||||
UNTERSUCHUNGSPHASEN (NIST Framework):
|
||||
${phasesDescription}
|
||||
|
||||
FORENSISCHE DOMÄNEN:
|
||||
${domainsDescription}
|
||||
|
||||
PRIORITÄTEN:
|
||||
1. Self-hosted Tools (projectUrl: "self-hosted") bevorzugen
|
||||
2. Open Source Tools bevorzugen (license != "Proprietary")
|
||||
3. Maximal 3 Tools pro Phase empfehlen
|
||||
4. Deutsche Antworten für deutsche Anfragen, English for English queries
|
||||
|
||||
ANTWORT-FORMAT (strict JSON):
|
||||
{
|
||||
"scenario_analysis": "Detaillierte Analyse des Szenarios auf Deutsch/English",
|
||||
"recommended_tools": [
|
||||
{
|
||||
"name": "EXAKTER Name aus der Database",
|
||||
"priority": "high|medium|low",
|
||||
"phase": "data-collection|examination|analysis|reporting",
|
||||
"justification": "Warum dieses Tool für dieses Szenario geeignet ist"
|
||||
}
|
||||
],
|
||||
"workflow_suggestion": "Vorgeschlagener Untersuchungsablauf",
|
||||
"additional_notes": "Wichtige Überlegungen und Hinweise"
|
||||
}
|
||||
|
||||
Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des JSON.`;
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
// Check if authentication is required
|
||||
const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
|
||||
let userId = 'test-user';
|
||||
|
||||
if (authRequired) {
|
||||
// Authentication check
|
||||
const sessionToken = getSessionFromRequest(request);
|
||||
if (!sessionToken) {
|
||||
return new Response(JSON.stringify({ error: 'Authentication required' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const session = await verifySession(sessionToken);
|
||||
if (!session) {
|
||||
return new Response(JSON.stringify({ error: 'Invalid session' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
userId = session.userId;
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
if (!checkRateLimit(userId)) {
|
||||
return new Response(JSON.stringify({ error: 'Rate limit exceeded' }), {
|
||||
status: 429,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
const body = await request.json();
|
||||
const { query } = body;
|
||||
|
||||
if (!query || typeof query !== 'string') {
|
||||
return new Response(JSON.stringify({ error: 'Query required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Sanitize input
|
||||
const sanitizedQuery = sanitizeInput(query);
|
||||
if (sanitizedQuery.includes('[FILTERED]')) {
|
||||
return new Response(JSON.stringify({ error: 'Invalid input detected' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Load tools database
|
||||
const toolsData = await loadToolsDatabase();
|
||||
|
||||
// Create AI request
|
||||
const systemPrompt = createSystemPrompt(toolsData);
|
||||
|
||||
const aiResponse = await fetch(process.env.AI_API_ENDPOINT + '/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.AI_API_KEY}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: AI_MODEL, // or whatever model is available
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: systemPrompt
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: sanitizedQuery
|
||||
}
|
||||
],
|
||||
max_tokens: 2000,
|
||||
temperature: 0.3
|
||||
})
|
||||
});
|
||||
|
||||
if (!aiResponse.ok) {
|
||||
console.error('AI API error:', await aiResponse.text());
|
||||
return new Response(JSON.stringify({ error: 'AI service unavailable' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const aiData = await aiResponse.json();
|
||||
const aiContent = aiData.choices?.[0]?.message?.content;
|
||||
|
||||
if (!aiContent) {
|
||||
return new Response(JSON.stringify({ error: 'No response from AI' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Parse AI JSON response
|
||||
let recommendation;
|
||||
try {
|
||||
const cleanedContent = stripMarkdownJson(aiContent);
|
||||
recommendation = JSON.parse(cleanedContent);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse AI response:', aiContent);
|
||||
return new Response(JSON.stringify({ error: 'Invalid AI response format' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Validate tool names against database
|
||||
const validToolNames = new Set(toolsData.tools.map((t: any) => t.name));
|
||||
const validatedRecommendation = {
|
||||
...recommendation,
|
||||
recommended_tools: recommendation.recommended_tools?.filter((tool: any) => {
|
||||
if (!validToolNames.has(tool.name)) {
|
||||
console.warn(`AI recommended unknown tool: ${tool.name}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}) || []
|
||||
};
|
||||
|
||||
// Log successful query
|
||||
console.log(`[AI Query] User: ${userId}, Query length: ${sanitizedQuery.length}, Tools: ${validatedRecommendation.recommended_tools.length}`);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
recommendation: validatedRecommendation,
|
||||
query: sanitizedQuery
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('AI query error:', error);
|
||||
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
100
src/pages/api/auth/callback.ts
Normal file
100
src/pages/api/auth/callback.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { parse } from 'cookie';
|
||||
import {
|
||||
exchangeCodeForTokens,
|
||||
getUserInfo,
|
||||
createSession,
|
||||
createSessionCookie,
|
||||
logAuthEvent
|
||||
} from '../../../utils/auth.js';
|
||||
|
||||
export const GET: APIRoute = async ({ url, request }) => {
|
||||
try {
|
||||
// Debug: multiple ways to access URL parameters
|
||||
console.log('Full URL:', url.toString());
|
||||
console.log('URL pathname:', url.pathname);
|
||||
console.log('URL search:', url.search);
|
||||
console.log('URL searchParams:', url.searchParams.toString());
|
||||
|
||||
// Try different ways to get parameters
|
||||
const allParams = Object.fromEntries(url.searchParams.entries());
|
||||
console.log('SearchParams entries:', allParams);
|
||||
|
||||
// Also try parsing manually from the search string
|
||||
const manualParams = new URLSearchParams(url.search);
|
||||
const manualEntries = Object.fromEntries(manualParams.entries());
|
||||
console.log('Manual URLSearchParams:', manualEntries);
|
||||
|
||||
// Also check request URL
|
||||
const requestUrl = new URL(request.url);
|
||||
console.log('Request URL:', requestUrl.toString());
|
||||
const requestParams = Object.fromEntries(requestUrl.searchParams.entries());
|
||||
console.log('Request URL params:', requestParams);
|
||||
|
||||
const code = url.searchParams.get('code') || requestUrl.searchParams.get('code');
|
||||
const state = url.searchParams.get('state') || requestUrl.searchParams.get('state');
|
||||
const error = url.searchParams.get('error') || requestUrl.searchParams.get('error');
|
||||
|
||||
console.log('Final extracted values:', { code: !!code, state: !!state, error });
|
||||
|
||||
// Handle OIDC errors
|
||||
if (error) {
|
||||
logAuthEvent('OIDC error', { error, description: url.searchParams.get('error_description') });
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: { 'Location': '/?auth=error' }
|
||||
});
|
||||
}
|
||||
|
||||
if (!code || !state) {
|
||||
logAuthEvent('Missing code or state parameter', { received: allParams });
|
||||
return new Response('Invalid callback parameters', { status: 400 });
|
||||
}
|
||||
|
||||
// Verify state parameter
|
||||
const cookieHeader = request.headers.get('cookie');
|
||||
const cookies = cookieHeader ? parse(cookieHeader) : {};
|
||||
const storedStateData = cookies.auth_state ? JSON.parse(decodeURIComponent(cookies.auth_state)) : null;
|
||||
|
||||
if (!storedStateData || storedStateData.state !== state) {
|
||||
logAuthEvent('State mismatch', { received: state, stored: storedStateData?.state });
|
||||
return new Response('Invalid state parameter', { status: 400 });
|
||||
}
|
||||
|
||||
// Exchange code for tokens
|
||||
const tokens = await exchangeCodeForTokens(code);
|
||||
|
||||
// Get user info
|
||||
const userInfo = await getUserInfo(tokens.access_token);
|
||||
|
||||
// Create session
|
||||
const sessionToken = await createSession(userInfo.sub || userInfo.preferred_username || 'unknown');
|
||||
const sessionCookie = createSessionCookie(sessionToken);
|
||||
|
||||
logAuthEvent('Authentication successful', {
|
||||
userId: userInfo.sub || userInfo.preferred_username,
|
||||
email: userInfo.email
|
||||
});
|
||||
|
||||
// Clear auth state cookie and redirect to intended destination
|
||||
const returnTo = storedStateData.returnTo || '/';
|
||||
const clearStateCookie = 'auth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0';
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append('Location', returnTo);
|
||||
headers.append('Set-Cookie', sessionCookie);
|
||||
headers.append('Set-Cookie', clearStateCookie);
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: headers
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logAuthEvent('Callback failed', { error: error.message });
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: { 'Location': '/?auth=error' }
|
||||
});
|
||||
}
|
||||
};
|
34
src/pages/api/auth/login.ts
Normal file
34
src/pages/api/auth/login.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { generateAuthUrl, generateState, logAuthEvent } from '../../../utils/auth.js';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const GET: APIRoute = async ({ url, redirect }) => {
|
||||
try {
|
||||
const state = generateState();
|
||||
const authUrl = generateAuthUrl(state);
|
||||
|
||||
// Debug: log the generated URL
|
||||
console.log('Generated auth URL:', authUrl);
|
||||
|
||||
// Get the intended destination after login (if any)
|
||||
const returnTo = url.searchParams.get('returnTo') || '/';
|
||||
|
||||
logAuthEvent('Login initiated', { returnTo, authUrl });
|
||||
|
||||
// Store state and returnTo in a cookie for the callback
|
||||
const stateData = JSON.stringify({ state, returnTo });
|
||||
const stateCookie = `auth_state=${encodeURIComponent(stateData)}; HttpOnly; SameSite=Lax; Path=/; Max-Age=600`; // 10 minutes
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
'Location': authUrl,
|
||||
'Set-Cookie': stateCookie
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logAuthEvent('Login failed', { error: error.message });
|
||||
return new Response('Authentication error', { status: 500 });
|
||||
}
|
||||
};
|
38
src/pages/api/auth/logout.ts
Normal file
38
src/pages/api/auth/logout.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const sessionToken = getSessionFromRequest(request);
|
||||
|
||||
if (!sessionToken) {
|
||||
return new Response(JSON.stringify({
|
||||
authenticated: false
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const session = await verifySession(sessionToken);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
authenticated: session !== null,
|
||||
expires: session?.exp ? new Date(session.exp * 1000).toISOString() : null
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({
|
||||
authenticated: false,
|
||||
error: 'Session verification failed'
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
104
src/pages/api/auth/process.ts
Normal file
104
src/pages/api/auth/process.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { parse } from 'cookie';
|
||||
import {
|
||||
exchangeCodeForTokens,
|
||||
getUserInfo,
|
||||
createSession,
|
||||
createSessionCookie,
|
||||
logAuthEvent
|
||||
} from '../../../utils/auth.js';
|
||||
|
||||
// Mark as server-rendered
|
||||
export const prerender = false;
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
// Check if there's a body to parse
|
||||
const contentType = request.headers.get('content-type');
|
||||
console.log('Request content-type:', contentType);
|
||||
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch (parseError) {
|
||||
console.error('JSON parse error:', parseError);
|
||||
return new Response(JSON.stringify({ success: false, error: 'Invalid JSON' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const { code, state } = body || {};
|
||||
|
||||
console.log('Processing authentication:', { code: !!code, state: !!state });
|
||||
|
||||
if (!code || !state) {
|
||||
logAuthEvent('Missing code or state parameter in process request');
|
||||
return new Response(JSON.stringify({ success: false }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Verify state parameter
|
||||
const cookieHeader = request.headers.get('cookie');
|
||||
const cookies = cookieHeader ? parse(cookieHeader) : {};
|
||||
const storedStateData = cookies.auth_state ? JSON.parse(decodeURIComponent(cookies.auth_state)) : null;
|
||||
|
||||
console.log('State verification:', {
|
||||
received: state,
|
||||
stored: storedStateData?.state,
|
||||
match: storedStateData?.state === state
|
||||
});
|
||||
|
||||
if (!storedStateData || storedStateData.state !== state) {
|
||||
logAuthEvent('State mismatch', { received: state, stored: storedStateData?.state });
|
||||
return new Response(JSON.stringify({ success: false }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Exchange code for tokens
|
||||
console.log('Exchanging code for tokens...');
|
||||
const tokens = await exchangeCodeForTokens(code);
|
||||
|
||||
// Get user info
|
||||
console.log('Getting user info...');
|
||||
const userInfo = await getUserInfo(tokens.access_token);
|
||||
|
||||
// Create session
|
||||
const sessionToken = await createSession(userInfo.sub || userInfo.preferred_username || 'unknown');
|
||||
const sessionCookie = createSessionCookie(sessionToken);
|
||||
|
||||
logAuthEvent('Authentication successful', {
|
||||
userId: userInfo.sub || userInfo.preferred_username,
|
||||
email: userInfo.email
|
||||
});
|
||||
|
||||
// Clear auth state cookie
|
||||
const clearStateCookie = 'auth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0';
|
||||
const returnTo = storedStateData.returnTo || '/';
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append('Content-Type', 'application/json');
|
||||
headers.append('Set-Cookie', sessionCookie);
|
||||
headers.append('Set-Cookie', clearStateCookie);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
redirectTo: returnTo
|
||||
}), {
|
||||
status: 200,
|
||||
headers: headers
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Authentication processing failed:', error);
|
||||
logAuthEvent('Authentication processing failed', { error: error.message });
|
||||
return new Response(JSON.stringify({ success: false }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
36
src/pages/api/auth/status.ts
Normal file
36
src/pages/api/auth/status.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const sessionToken = getSessionFromRequest(request);
|
||||
|
||||
if (!sessionToken) {
|
||||
return new Response(JSON.stringify({
|
||||
authenticated: false
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const session = await verifySession(sessionToken);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
authenticated: session !== null,
|
||||
expires: session?.exp ? new Date(session.exp * 1000).toISOString() : null
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({
|
||||
authenticated: false,
|
||||
error: 'Session verification failed'
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
54
src/pages/auth/callback.astro
Normal file
54
src/pages/auth/callback.astro
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
// Since server-side URL parameters aren't working,
|
||||
// we'll handle this client-side and POST to the API
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Processing Authentication...</title>
|
||||
</head>
|
||||
<body>
|
||||
<div style="text-align: center; padding: 4rem; font-family: sans-serif;">
|
||||
<h2>Processing authentication...</h2>
|
||||
<p>Please wait while we complete your login.</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Get URL parameters from client-side
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
const state = urlParams.get('state');
|
||||
const error = urlParams.get('error');
|
||||
|
||||
console.log('Client-side callback params:', { code: !!code, state: !!state, error });
|
||||
|
||||
if (error) {
|
||||
window.location.href = '/?auth=error';
|
||||
} else if (code && state) {
|
||||
// Send the parameters to our API endpoint
|
||||
fetch('/api/auth/process', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ code, state })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.href = data.redirectTo || '/';
|
||||
} else {
|
||||
window.location.href = '/?auth=error';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Authentication processing failed:', error);
|
||||
window.location.href = '/?auth=error';
|
||||
});
|
||||
} else {
|
||||
console.error('Missing code or state parameters');
|
||||
window.location.href = '/?auth=error';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
176
src/utils/auth.ts
Normal file
176
src/utils/auth.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import { SignJWT, jwtVerify, type JWTPayload } from 'jose';
|
||||
import { serialize, parse } from 'cookie';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
// Load environment variables
|
||||
config();
|
||||
|
||||
// Environment variables - use runtime access for server-side
|
||||
function getEnv(key: string): string {
|
||||
const value = process.env[key];
|
||||
if (!value) {
|
||||
throw new Error(`Missing environment variable: ${key}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const SECRET_KEY = new TextEncoder().encode(getEnv('OIDC_CLIENT_SECRET'));
|
||||
const SESSION_DURATION = 6 * 60 * 60; // 6 hours in seconds
|
||||
|
||||
export interface SessionData {
|
||||
userId: string;
|
||||
authenticated: boolean;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
// Create a signed JWT session token
|
||||
export async function createSession(userId: string): Promise<string> {
|
||||
const exp = Math.floor(Date.now() / 1000) + SESSION_DURATION;
|
||||
|
||||
return await new SignJWT({
|
||||
userId,
|
||||
authenticated: true,
|
||||
exp
|
||||
})
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setExpirationTime(exp)
|
||||
.sign(SECRET_KEY);
|
||||
}
|
||||
|
||||
// Verify and decode a session token
|
||||
export async function verifySession(token: string): Promise<SessionData | null> {
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, SECRET_KEY);
|
||||
|
||||
// Validate payload structure and cast properly
|
||||
if (
|
||||
typeof payload.userId === 'string' &&
|
||||
typeof payload.authenticated === 'boolean' &&
|
||||
typeof payload.exp === 'number'
|
||||
) {
|
||||
return {
|
||||
userId: payload.userId,
|
||||
authenticated: payload.authenticated,
|
||||
exp: payload.exp
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.log('Session verification failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get session from request cookies
|
||||
export function getSessionFromRequest(request: Request): string | null {
|
||||
const cookieHeader = request.headers.get('cookie');
|
||||
if (!cookieHeader) return null;
|
||||
|
||||
const cookies = parse(cookieHeader);
|
||||
return cookies.session || null;
|
||||
}
|
||||
|
||||
// Create session cookie
|
||||
export function createSessionCookie(token: string): string {
|
||||
const publicBaseUrl = getEnv('PUBLIC_BASE_URL');
|
||||
const isSecure = publicBaseUrl.startsWith('https://');
|
||||
|
||||
return serialize('session', token, {
|
||||
httpOnly: true,
|
||||
secure: isSecure,
|
||||
sameSite: 'lax',
|
||||
maxAge: SESSION_DURATION,
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
|
||||
// Clear session cookie
|
||||
export function clearSessionCookie(): string {
|
||||
const publicBaseUrl = getEnv('PUBLIC_BASE_URL');
|
||||
const isSecure = publicBaseUrl.startsWith('https://');
|
||||
|
||||
return serialize('session', '', {
|
||||
httpOnly: true,
|
||||
secure: isSecure,
|
||||
sameSite: 'lax',
|
||||
maxAge: 0,
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
|
||||
// Generate OIDC authorization URL
|
||||
export function generateAuthUrl(state: string): string {
|
||||
const oidcEndpoint = getEnv('OIDC_ENDPOINT');
|
||||
const clientId = getEnv('OIDC_CLIENT_ID');
|
||||
const publicBaseUrl = getEnv('PUBLIC_BASE_URL');
|
||||
|
||||
const params = new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: clientId,
|
||||
redirect_uri: `${publicBaseUrl}/auth/callback`,
|
||||
scope: 'openid profile email',
|
||||
state: state
|
||||
});
|
||||
|
||||
return `${oidcEndpoint}/apps/oidc/authorize?${params.toString()}`;
|
||||
}
|
||||
|
||||
// Exchange authorization code for tokens
|
||||
export async function exchangeCodeForTokens(code: string): Promise<any> {
|
||||
const oidcEndpoint = getEnv('OIDC_ENDPOINT');
|
||||
const clientId = getEnv('OIDC_CLIENT_ID');
|
||||
const clientSecret = getEnv('OIDC_CLIENT_SECRET');
|
||||
const publicBaseUrl = getEnv('PUBLIC_BASE_URL');
|
||||
|
||||
const response = await fetch(`${oidcEndpoint}/apps/oidc/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
redirect_uri: `${publicBaseUrl}/auth/callback`
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
console.error('Token exchange failed:', error);
|
||||
throw new Error('Failed to exchange authorization code');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Get user info from OIDC provider
|
||||
export async function getUserInfo(accessToken: string): Promise<any> {
|
||||
const oidcEndpoint = getEnv('OIDC_ENDPOINT');
|
||||
|
||||
const response = await fetch(`${oidcEndpoint}/apps/oidc/userinfo`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
console.error('Userinfo request failed:', error);
|
||||
throw new Error('Failed to get user info');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Generate random state for CSRF protection
|
||||
export function generateState(): string {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
// Log authentication events for debugging
|
||||
export function logAuthEvent(event: string, details?: any) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.log(`[AUTH ${timestamp}] ${event}`, details ? JSON.stringify(details) : '');
|
||||
}
|
40
src/utils/serverAuth.ts
Normal file
40
src/utils/serverAuth.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import type { AstroGlobal } from 'astro';
|
||||
import { getSessionFromRequest, verifySession, type SessionData } from './auth.js';
|
||||
|
||||
export interface AuthContext {
|
||||
authenticated: boolean;
|
||||
session: SessionData | null;
|
||||
}
|
||||
|
||||
// Check authentication status for server-side pages
|
||||
export async function getAuthContext(Astro: AstroGlobal): Promise<AuthContext> {
|
||||
try {
|
||||
const sessionToken = getSessionFromRequest(Astro.request);
|
||||
|
||||
if (!sessionToken) {
|
||||
return { authenticated: false, session: null };
|
||||
}
|
||||
|
||||
const session = await verifySession(sessionToken);
|
||||
|
||||
return {
|
||||
authenticated: session !== null,
|
||||
session
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to get auth context:', error);
|
||||
return { authenticated: false, session: null };
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
export function requireAuth(authContext: AuthContext, currentUrl: string): Response | null {
|
||||
if (!authContext.authenticated) {
|
||||
const loginUrl = `/api/auth/login?returnTo=${encodeURIComponent(currentUrl)}`;
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: { 'Location': loginUrl }
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user