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