diff --git a/.env b/.env new file mode 100644 index 000000000..01162e6de --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +# These are non-sensitive variables, hence they are in git. +PUBLIC_ADMIN_URL=http://localhost:5174 +PUBLIC_PLAYWRIGHT= \ No newline at end of file diff --git a/.env.example b/.env.example deleted file mode 100644 index 1ba7cff39..000000000 --- a/.env.example +++ /dev/null @@ -1,16 +0,0 @@ -# When cloning the repository, copy this file as .env -# For getting started, you don't need to change any values - -APP_DB_URL=file:databases/app/app.db -APP_DB_AUTH_TOKEN= - -REDIS_URL=redis://127.0.0.1:6379 - -PUBLIC_PLAYWRIGHT= - -ADMIN_PAGE_PASSWORD=test -GITHUB_PRIVATE_KEY= -EMAIL_ADDRESS= -EMAIL_PASSWORD= -ENABLE_EMAILS=false -APPROVAL_EMAIL= \ No newline at end of file diff --git a/.github/workflows/_deploy-reusable.yaml b/.github/workflows/_deploy-reusable.yaml index ef1ed0f82..929ac5ac1 100644 --- a/.github/workflows/_deploy-reusable.yaml +++ b/.github/workflows/_deploy-reusable.yaml @@ -7,28 +7,10 @@ on: required: true type: boolean secrets: - APP_DB_URL: - required: true - APP_DB_AUTH_TOKEN: - required: true NETLIFY_SITE_ID: required: true NETLIFY_ACCESS_TOKEN: required: true - _GITHUB_PRIVATE_KEY: - required: true - REDIS_URL: - required: true - ADMIN_PAGE_PASSWORD: - required: true - EMAIL_ADDRESS: - required: true - EMAIL_PASSWORD: - required: true - ENABLE_EMAILS: - required: true - APPROVAL_EMAIL: - required: true jobs: build-and-deploy: @@ -56,25 +38,14 @@ jobs: run: pnpm i - name: Build database - env: - APP_DB_URL: ${{ secrets.APP_DB_URL }} - APP_DB_AUTH_TOKEN: ${{ secrets.APP_DB_AUTH_TOKEN }} run: | pnpm db:setup pnpm db:update - name: Build app env: - APP_DB_URL: ${{ secrets.APP_DB_URL }} - APP_DB_AUTH_TOKEN: ${{ secrets.APP_DB_AUTH_TOKEN }} - GITHUB_PRIVATE_KEY: ${{ secrets._GITHUB_PRIVATE_KEY }} - REDIS_URL: ${{ secrets.REDIS_URL }} - ADMIN_PAGE_PASSWORD: ${{ secrets.ADMIN_PAGE_PASSWORD }} - EMAIL_ADDRESS: ${{ secrets.EMAIL_ADDRESS }} - EMAIL_PASSWORD: ${{ secrets.EMAIL_PASSWORD }} - ENABLE_EMAILS: ${{ secrets.ENABLE_EMAILS }} - APPROVAL_EMAIL: ${{ secrets.APPROVAL_EMAIL }} PUBLIC_PLAYWRIGHT: '' + PUBLIC_ADMIN_URL: ${{ vars.PUBLIC_ADMIN_URL }} run: pnpm build - name: Get latest commit message diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 61e6b5934..7693582d0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -31,7 +31,7 @@ jobs: - name: Test database run: | - pnpm db:setup:catdat + pnpm db:setup pnpm db:update - name: Cache Playwright browsers @@ -44,4 +44,6 @@ jobs: run: pnpm exec playwright install --with-deps chromium - name: Run Playwright tests + env: + PUBLIC_ADMIN_URL: http://localhost:5174 run: pnpm e2e diff --git a/.gitignore b/.gitignore index d97fa06ef..807f3ee3a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,11 +12,6 @@ node_modules .DS_Store Thumbs.db -# Env -.env -.env.* -!.env.example -!.env.test # Vite vite.config.js.timestamp-* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f630d3fad..a3f4accc3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,12 +47,9 @@ You need to have [Git](https://git-scm.com/), [NodeJS](https://nodejs.org/) and 2. Clone your fork with `git clone https://github.com/{your_username}/CatDat.git`. 3. Change into the directory with `cd CatDat`. 4. Install dependencies with `pnpm install`. -5. Create a local `.env` file from `.env.example`. -6. Create the local database with `pnpm db:setup`. -7. Update the local database with `pnpm db:update`. -8. Start the local development server with `pnpm dev`. - -**For Maintainers:** Redis is required for admin features and submissions. Start a local instance on port 6379 via `redis-server --port 6379`. +5. Create the local database with `pnpm db:setup`. +6. Update the local database with `pnpm db:update`. +7. Start the local development server with `pnpm dev`. ### Updating the Database diff --git a/DATABASE.md b/DATABASE.md index 7f237d89c..61cb50c1c 100644 --- a/DATABASE.md +++ b/DATABASE.md @@ -111,7 +111,3 @@ to check for redundant assignments of properties to categorical structures. This is the database schema as of 01.07.2026; changes may occur. database diagram - -## Application Database - -The application itself uses another database to store user submissions and page visits. The local copy of this database is at `/databases/app/app.db`. diff --git a/README.md b/README.md index 4f8b0dc27..016120abf 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,9 @@ You need to have [Git](https://git-scm.com/), [NodeJS](https://nodejs.org/) and 2. Clone your fork with `git clone https://github.com/{your_username}/CatDat.git`. 3. Change into the directory with `cd CatDat`. 4. Install dependencies with `pnpm install`. -5. Create a local `.env` file from `.env.example`. -6. Create the local database with `pnpm db:setup`. -7. Update the local database with `pnpm db:update`. -8. Start the local development server with `pnpm dev`. - -**For Maintainers:** Redis is required for admin features and submissions. Start a local instance on port 6379 via `redis-server --port 6379`. +5. Create the local database with `pnpm db:setup`. +6. Update the local database with `pnpm db:update`. +7. Start the local development server with `pnpm dev`. ## Tech Stack @@ -61,6 +58,10 @@ Built with modern web technologies: - Math Rendering: [katex](https://www.npmjs.com/package/katex) - End-to-end testing: [Playwright](https://playwright.dev) +## Admin application + +The repository [CatDatAdmin](https://github.com/ScriptRaccoon/CatDatAdmin) contains the admin functionality for CatDat. + ## Similar projects _CatDat_ draws inspiration from and complements other resources in category theory: diff --git a/databases/app/scripts/setup.ts b/databases/app/scripts/setup.ts deleted file mode 100644 index 7bcc51691..000000000 --- a/databases/app/scripts/setup.ts +++ /dev/null @@ -1,79 +0,0 @@ -import dotenv from 'dotenv' -import { createClient } from '@libsql/client' -import { devlog } from '$shared/utils' - -dotenv.config({ quiet: true }) - -const APP_DB_URL = process.env.APP_DB_URL -const APP_DB_AUTH_TOKEN = process.env.APP_DB_AUTH_TOKEN - -if (!APP_DB_URL) throw new Error('No APP_DB_URL found') - -const db = createClient({ - url: APP_DB_URL, - authToken: APP_DB_AUTH_TOKEN -}) - -setup() - -/** - * Creates the tables in the app database. - */ -async function setup() { - console.info('\n--- Setup App database ---') - await create_visits_table() - await create_submissions_table() - await create_actions_table() -} - -/** - * Creates the page visits table. - */ -async function create_visits_table() { - await db.execute(` - CREATE TABLE IF NOT EXISTS visits ( - id INTEGER PRIMARY KEY, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - theme TEXT NOT NULL CHECK (theme in ('dark', 'light')), - device_type TEXT NOT NULL CHECK (device_type in ('mobile', 'tablet', 'desktop')), - country TEXT - ) - `) - - devlog('Visits table ready') -} - -/** - * Creates the table of submissions. - */ -async function create_submissions_table() { - await db.execute(` - CREATE TABLE IF NOT EXISTS submissions ( - id INTEGER PRIMARY KEY, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - title TEXT NOT NULL, - body TEXT NOT NULL, - name TEXT, - url TEXT NOT NULL, - approved_at TEXT - ) - `) - - devlog('Submissions table ready') -} - -/** - * Creates the user actions table, used to record anonymously various user actions. - */ -async function create_actions_table() { - await db.execute(` - CREATE TABLE IF NOT EXISTS user_actions ( - id INTEGER PRIMARY KEY, - action TEXT NOT NULL, - value TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ) - `) - - devlog('User actions table ready') -} diff --git a/package.json b/package.json index 6e756261c..df7e0186f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.1", "type": "module", "scripts": { - "dev": "pnpm env:check && vite dev", + "dev": "vite dev", "dev:remote": "vite dev --mode prod", "dev:host": "vite dev --host", "build": "vite build", @@ -14,11 +14,9 @@ "format": "prettier --write .", "lint": "prettier --check .", "prepare": "husky", - "env:check": "tsx scripts/env.check", "db:shell": "sqlite3 databases/catdat/catdat.db", "db:kill": "rm -rf databases/catdat/catdat.db", - "db:setup:catdat": "pnpm db:kill && tsx --tsconfig databases/tsconfig.json databases/catdat/scripts/setup.ts", - "db:setup": "pnpm db:kill && tsx --tsconfig databases/tsconfig.json databases/catdat/scripts/setup.ts && tsx --tsconfig databases/tsconfig.json databases/app/scripts/setup.ts", + "db:setup": "pnpm db:kill && tsx --tsconfig databases/tsconfig.json databases/catdat/scripts/setup.ts", "db:seed": "tsx --tsconfig databases/tsconfig.json databases/catdat/scripts/seed.ts", "db:deduce": "tsx --tsconfig databases/tsconfig.json databases/catdat/scripts/deduce.ts", "db:test": "tsx --tsconfig databases/tsconfig.json databases/catdat/scripts/test.ts", @@ -40,7 +38,6 @@ "@types/katex": "^0.16.8", "@types/markdown-it": "^14.1.2", "@types/node": "^25.5.0", - "@types/nodemailer": "^8.0.0", "better-sqlite3": "^12.9.0", "chokidar": "^5.0.0", "dotenv": "^17.3.1", @@ -62,12 +59,6 @@ "dependencies": { "@fortawesome/free-regular-svg-icons": "^7.2.0", "@fortawesome/free-solid-svg-icons": "^7.2.0", - "@libsql/client": "^0.17.2", - "@octokit/app": "^16.1.2", - "es6-crawler-detect": "^4.0.2", - "ioredis": "^5.10.1", - "nodemailer": "^8.0.5", - "sql-template-tag": "^5.2.1", "svelte-fa": "^4.0.4", "yaml": "^2.8.4" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f62ff1e3f..a79ac4675 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,24 +17,6 @@ importers: '@fortawesome/free-solid-svg-icons': specifier: ^7.2.0 version: 7.2.0 - '@libsql/client': - specifier: ^0.17.2 - version: 0.17.3 - '@octokit/app': - specifier: ^16.1.2 - version: 16.1.2 - es6-crawler-detect: - specifier: ^4.0.2 - version: 4.0.2 - ioredis: - specifier: ^5.10.1 - version: 5.10.1 - nodemailer: - specifier: ^8.0.5 - version: 8.0.7 - sql-template-tag: - specifier: ^5.2.1 - version: 5.2.1 svelte-fa: specifier: ^4.0.4 version: 4.0.4(svelte@5.55.7) @@ -66,9 +48,6 @@ importers: '@types/node': specifier: ^25.5.0 version: 25.6.0 - '@types/nodemailer': - specifier: ^8.0.0 - version: 8.0.0 better-sqlite3: specifier: ^12.9.0 version: 12.9.0 @@ -450,9 +429,6 @@ packages: '@iarna/toml@2.2.5': resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} - '@ioredis/commands@1.5.1': - resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -469,155 +445,12 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@libsql/client@0.17.3': - resolution: {integrity: sha512-HXk9wiAoJbKFbyBH4O+aEhN6ir5ERXuXvwE5OD2eR4/5RUa3Pw/8L9zrnVdU+iNJitRvisPWaIwmhkO3bH7giA==} - - '@libsql/core@0.17.3': - resolution: {integrity: sha512-2UjK1i7JBkMduJo4WdvvBxMMvVJ31pArBZNONyz/GCJJAH+1UHat2X6vn10S/WpY5fKzIT98WqYFl2vzWRLOfg==} - - '@libsql/darwin-arm64@0.5.29': - resolution: {integrity: sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==} - cpu: [arm64] - os: [darwin] - - '@libsql/darwin-x64@0.5.29': - resolution: {integrity: sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==} - cpu: [x64] - os: [darwin] - - '@libsql/hrana-client@0.10.0': - resolution: {integrity: sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw==} - - '@libsql/isomorphic-ws@0.1.5': - resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} - - '@libsql/linux-arm-gnueabihf@0.5.29': - resolution: {integrity: sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==} - cpu: [arm] - os: [linux] - - '@libsql/linux-arm-musleabihf@0.5.29': - resolution: {integrity: sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==} - cpu: [arm] - os: [linux] - - '@libsql/linux-arm64-gnu@0.5.29': - resolution: {integrity: sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==} - cpu: [arm64] - os: [linux] - - '@libsql/linux-arm64-musl@0.5.29': - resolution: {integrity: sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==} - cpu: [arm64] - os: [linux] - - '@libsql/linux-x64-gnu@0.5.29': - resolution: {integrity: sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==} - cpu: [x64] - os: [linux] - - '@libsql/linux-x64-musl@0.5.29': - resolution: {integrity: sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==} - cpu: [x64] - os: [linux] - - '@libsql/win32-x64-msvc@0.5.29': - resolution: {integrity: sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==} - cpu: [x64] - os: [win32] - '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@neon-rs/load@0.0.4': - resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} - - '@octokit/app@16.1.2': - resolution: {integrity: sha512-8j7sEpUYVj18dxvh0KWj6W/l6uAiVRBl1JBDVRqH1VHKAO/G5eRVl4yEoYACjakWers1DjUkcCHyJNQK47JqyQ==} - engines: {node: '>= 20'} - - '@octokit/auth-app@8.2.0': - resolution: {integrity: sha512-vVjdtQQwomrZ4V46B9LaCsxsySxGoHsyw6IYBov/TqJVROrlYdyNgw5q6tQbB7KZt53v1l1W53RiqTvpzL907g==} - engines: {node: '>= 20'} - - '@octokit/auth-oauth-app@9.0.3': - resolution: {integrity: sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg==} - engines: {node: '>= 20'} - - '@octokit/auth-oauth-device@8.0.3': - resolution: {integrity: sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw==} - engines: {node: '>= 20'} - - '@octokit/auth-oauth-user@6.0.2': - resolution: {integrity: sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A==} - engines: {node: '>= 20'} - - '@octokit/auth-token@6.0.0': - resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} - engines: {node: '>= 20'} - - '@octokit/auth-unauthenticated@7.0.3': - resolution: {integrity: sha512-8Jb1mtUdmBHL7lGmop9mU9ArMRUTRhg8vp0T1VtZ4yd9vEm3zcLwmjQkhNEduKawOOORie61xhtYIhTDN+ZQ3g==} - engines: {node: '>= 20'} - - '@octokit/core@7.0.6': - resolution: {integrity: sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==} - engines: {node: '>= 20'} - - '@octokit/endpoint@11.0.3': - resolution: {integrity: sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==} - engines: {node: '>= 20'} - - '@octokit/graphql@9.0.3': - resolution: {integrity: sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==} - engines: {node: '>= 20'} - - '@octokit/oauth-app@8.0.3': - resolution: {integrity: sha512-jnAjvTsPepyUaMu9e69hYBuozEPgYqP4Z3UnpmvoIzHDpf8EXDGvTY1l1jK0RsZ194oRd+k6Hm13oRU8EoDFwg==} - engines: {node: '>= 20'} - - '@octokit/oauth-authorization-url@8.0.0': - resolution: {integrity: sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==} - engines: {node: '>= 20'} - - '@octokit/oauth-methods@6.0.2': - resolution: {integrity: sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng==} - engines: {node: '>= 20'} - - '@octokit/openapi-types@27.0.0': - resolution: {integrity: sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==} - - '@octokit/openapi-webhooks-types@12.1.0': - resolution: {integrity: sha512-WiuzhOsiOvb7W3Pvmhf8d2C6qaLHXrWiLBP4nJ/4kydu+wpagV5Fkz9RfQwV2afYzv3PB+3xYgp4mAdNGjDprA==} - - '@octokit/plugin-paginate-rest@14.0.0': - resolution: {integrity: sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==} - engines: {node: '>= 20'} - peerDependencies: - '@octokit/core': '>=6' - - '@octokit/request-error@7.1.0': - resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==} - engines: {node: '>= 20'} - - '@octokit/request@10.0.8': - resolution: {integrity: sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==} - engines: {node: '>= 20'} - - '@octokit/types@16.0.0': - resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} - - '@octokit/webhooks-methods@6.0.0': - resolution: {integrity: sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==} - engines: {node: '>= 20'} - - '@octokit/webhooks@14.2.0': - resolution: {integrity: sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw==} - engines: {node: '>= 20'} - '@oxc-project/types@0.130.0': resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==} @@ -766,9 +599,6 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@types/aws-lambda@8.10.161': - resolution: {integrity: sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==} - '@types/better-sqlite3@7.6.13': resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} @@ -793,15 +623,9 @@ packages: '@types/node@25.6.0': resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} - '@types/nodemailer@8.0.0': - resolution: {integrity: sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==} - '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@types/ws@8.18.1': - resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -824,9 +648,6 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - before-after-hook@4.0.0: - resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} - better-sqlite3@12.9.0: resolution: {integrity: sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==} engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} @@ -855,10 +676,6 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - cluster-key-slot@1.1.2: - resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} - engines: {node: '>=0.10.0'} - commander@8.3.0: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} @@ -867,15 +684,6 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -888,14 +696,6 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - denque@2.1.0: - resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} - engines: {node: '>=0.10'} - - detect-libc@2.0.2: - resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} - engines: {node: '>=8'} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -914,9 +714,6 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - es6-crawler-detect@4.0.2: - resolution: {integrity: sha512-ET8rHP97ifJtkImZJ6Pul/mB25hEKQNSVM592pWmeumu3DBJ+LnhVWDahfsgbjvZL/78W4EupYPBAZijbwPi4g==} - esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -951,9 +748,6 @@ packages: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} - fast-content-type-parse@3.0.0: - resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1003,10 +797,6 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - ioredis@5.10.1: - resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} - engines: {node: '>=12.22.0'} - is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} @@ -1014,16 +804,10 @@ packages: is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - js-base64@3.7.8: - resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} - js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true - json-with-bigint@3.5.8: - resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} - katex@0.16.45: resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} hasBin: true @@ -1036,11 +820,6 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - libsql@0.5.29: - resolution: {integrity: sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==} - cpu: [x64, arm64, wasm32, arm] - os: [darwin, linux, win32] - lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -1121,12 +900,6 @@ packages: locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} - lodash.defaults@4.2.0: - resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - - lodash.isarguments@3.1.0: - resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1155,9 +928,6 @@ packages: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1170,10 +940,6 @@ packages: resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==} engines: {node: '>=10'} - nodemailer@8.0.7: - resolution: {integrity: sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==} - engines: {node: '>=6.0.0'} - obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -1218,9 +984,6 @@ packages: engines: {node: '>=14'} hasBin: true - promise-limit@2.7.0: - resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} - pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -1244,14 +1007,6 @@ packages: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} - redis-errors@1.2.0: - resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} - engines: {node: '>=4'} - - redis-parser@3.0.0: - resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} - engines: {node: '>=4'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -1296,13 +1051,6 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sql-template-tag@5.2.1: - resolution: {integrity: sha512-lFdvXCOqWhV40A7w4oQVDyuaNFb5yO+dhsHStZzOdtDJWCBWYv4+hhATK5nPpY5v/T1OMVcLMPeN4519qIyb9Q==} - engines: {node: '>=14'} - - standard-as-callback@2.1.0: - resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -1342,10 +1090,6 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} - toad-cache@3.7.0: - resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} - engines: {node: '>=12'} - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -1372,12 +1116,6 @@ packages: undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} - universal-github-app-jwt@2.2.2: - resolution: {integrity: sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==} - - universal-user-agent@7.0.3: - resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -1435,18 +1173,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - yaml@2.8.4: resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==} engines: {node: '>= 14.6'} @@ -1641,8 +1367,6 @@ snapshots: '@iarna/toml@2.2.5': {} - '@ioredis/commands@1.5.1': {} - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1662,64 +1386,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@libsql/client@0.17.3': - dependencies: - '@libsql/core': 0.17.3 - '@libsql/hrana-client': 0.10.0 - js-base64: 3.7.8 - libsql: 0.5.29 - promise-limit: 2.7.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@libsql/core@0.17.3': - dependencies: - js-base64: 3.7.8 - - '@libsql/darwin-arm64@0.5.29': - optional: true - - '@libsql/darwin-x64@0.5.29': - optional: true - - '@libsql/hrana-client@0.10.0': - dependencies: - '@libsql/isomorphic-ws': 0.1.5 - js-base64: 3.7.8 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@libsql/isomorphic-ws@0.1.5': - dependencies: - '@types/ws': 8.18.1 - ws: 8.20.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@libsql/linux-arm-gnueabihf@0.5.29': - optional: true - - '@libsql/linux-arm-musleabihf@0.5.29': - optional: true - - '@libsql/linux-arm64-gnu@0.5.29': - optional: true - - '@libsql/linux-arm64-musl@0.5.29': - optional: true - - '@libsql/linux-x64-gnu@0.5.29': - optional: true - - '@libsql/linux-x64-musl@0.5.29': - optional: true - - '@libsql/win32-x64-msvc@0.5.29': - optional: true - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -1727,134 +1393,6 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@neon-rs/load@0.0.4': {} - - '@octokit/app@16.1.2': - dependencies: - '@octokit/auth-app': 8.2.0 - '@octokit/auth-unauthenticated': 7.0.3 - '@octokit/core': 7.0.6 - '@octokit/oauth-app': 8.0.3 - '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) - '@octokit/types': 16.0.0 - '@octokit/webhooks': 14.2.0 - - '@octokit/auth-app@8.2.0': - dependencies: - '@octokit/auth-oauth-app': 9.0.3 - '@octokit/auth-oauth-user': 6.0.2 - '@octokit/request': 10.0.8 - '@octokit/request-error': 7.1.0 - '@octokit/types': 16.0.0 - toad-cache: 3.7.0 - universal-github-app-jwt: 2.2.2 - universal-user-agent: 7.0.3 - - '@octokit/auth-oauth-app@9.0.3': - dependencies: - '@octokit/auth-oauth-device': 8.0.3 - '@octokit/auth-oauth-user': 6.0.2 - '@octokit/request': 10.0.8 - '@octokit/types': 16.0.0 - universal-user-agent: 7.0.3 - - '@octokit/auth-oauth-device@8.0.3': - dependencies: - '@octokit/oauth-methods': 6.0.2 - '@octokit/request': 10.0.8 - '@octokit/types': 16.0.0 - universal-user-agent: 7.0.3 - - '@octokit/auth-oauth-user@6.0.2': - dependencies: - '@octokit/auth-oauth-device': 8.0.3 - '@octokit/oauth-methods': 6.0.2 - '@octokit/request': 10.0.8 - '@octokit/types': 16.0.0 - universal-user-agent: 7.0.3 - - '@octokit/auth-token@6.0.0': {} - - '@octokit/auth-unauthenticated@7.0.3': - dependencies: - '@octokit/request-error': 7.1.0 - '@octokit/types': 16.0.0 - - '@octokit/core@7.0.6': - dependencies: - '@octokit/auth-token': 6.0.0 - '@octokit/graphql': 9.0.3 - '@octokit/request': 10.0.8 - '@octokit/request-error': 7.1.0 - '@octokit/types': 16.0.0 - before-after-hook: 4.0.0 - universal-user-agent: 7.0.3 - - '@octokit/endpoint@11.0.3': - dependencies: - '@octokit/types': 16.0.0 - universal-user-agent: 7.0.3 - - '@octokit/graphql@9.0.3': - dependencies: - '@octokit/request': 10.0.8 - '@octokit/types': 16.0.0 - universal-user-agent: 7.0.3 - - '@octokit/oauth-app@8.0.3': - dependencies: - '@octokit/auth-oauth-app': 9.0.3 - '@octokit/auth-oauth-user': 6.0.2 - '@octokit/auth-unauthenticated': 7.0.3 - '@octokit/core': 7.0.6 - '@octokit/oauth-authorization-url': 8.0.0 - '@octokit/oauth-methods': 6.0.2 - '@types/aws-lambda': 8.10.161 - universal-user-agent: 7.0.3 - - '@octokit/oauth-authorization-url@8.0.0': {} - - '@octokit/oauth-methods@6.0.2': - dependencies: - '@octokit/oauth-authorization-url': 8.0.0 - '@octokit/request': 10.0.8 - '@octokit/request-error': 7.1.0 - '@octokit/types': 16.0.0 - - '@octokit/openapi-types@27.0.0': {} - - '@octokit/openapi-webhooks-types@12.1.0': {} - - '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.6)': - dependencies: - '@octokit/core': 7.0.6 - '@octokit/types': 16.0.0 - - '@octokit/request-error@7.1.0': - dependencies: - '@octokit/types': 16.0.0 - - '@octokit/request@10.0.8': - dependencies: - '@octokit/endpoint': 11.0.3 - '@octokit/request-error': 7.1.0 - '@octokit/types': 16.0.0 - fast-content-type-parse: 3.0.0 - json-with-bigint: 3.5.8 - universal-user-agent: 7.0.3 - - '@octokit/types@16.0.0': - dependencies: - '@octokit/openapi-types': 27.0.0 - - '@octokit/webhooks-methods@6.0.0': {} - - '@octokit/webhooks@14.2.0': - dependencies: - '@octokit/openapi-webhooks-types': 12.1.0 - '@octokit/request-error': 7.1.0 - '@octokit/webhooks-methods': 6.0.0 - '@oxc-project/types@0.130.0': {} '@playwright/test@1.61.1': @@ -1960,8 +1498,6 @@ snapshots: tslib: 2.8.1 optional: true - '@types/aws-lambda@8.10.161': {} - '@types/better-sqlite3@7.6.13': dependencies: '@types/node': 25.6.0 @@ -1985,16 +1521,8 @@ snapshots: dependencies: undici-types: 7.19.2 - '@types/nodemailer@8.0.0': - dependencies: - '@types/node': 25.6.0 - '@types/trusted-types@2.0.7': {} - '@types/ws@8.18.1': - dependencies: - '@types/node': 25.6.0 - acorn@8.16.0: {} argparse@1.0.10: @@ -2009,8 +1537,6 @@ snapshots: base64-js@1.5.1: {} - before-after-hook@4.0.0: {} - better-sqlite3@12.9.0: dependencies: bindings: 1.5.0 @@ -2043,16 +1569,10 @@ snapshots: clsx@2.1.1: {} - cluster-key-slot@1.1.2: {} - commander@8.3.0: {} cookie@1.1.1: {} - debug@4.4.3: - dependencies: - ms: 2.1.3 - decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -2061,10 +1581,6 @@ snapshots: deepmerge@4.3.1: {} - denque@2.1.0: {} - - detect-libc@2.0.2: {} - detect-libc@2.1.2: {} devalue@5.8.1: {} @@ -2077,8 +1593,6 @@ snapshots: entities@4.5.0: {} - es6-crawler-detect@4.0.2: {} - esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -2151,8 +1665,6 @@ snapshots: dependencies: is-extendable: 0.1.1 - fast-content-type-parse@3.0.0: {} - fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -2188,35 +1700,17 @@ snapshots: ini@1.3.8: {} - ioredis@5.10.1: - dependencies: - '@ioredis/commands': 1.5.1 - cluster-key-slot: 1.1.2 - debug: 4.4.3 - denque: 2.1.0 - lodash.defaults: 4.2.0 - lodash.isarguments: 3.1.0 - redis-errors: 1.2.0 - redis-parser: 3.0.0 - standard-as-callback: 2.1.0 - transitivePeerDependencies: - - supports-color - is-extendable@0.1.1: {} is-reference@3.0.3: dependencies: '@types/estree': 1.0.8 - js-base64@3.7.8: {} - js-yaml@3.14.2: dependencies: argparse: 1.0.10 esprima: 4.0.1 - json-with-bigint@3.5.8: {} - katex@0.16.45: dependencies: commander: 8.3.0 @@ -2225,21 +1719,6 @@ snapshots: kleur@4.1.5: {} - libsql@0.5.29: - dependencies: - '@neon-rs/load': 0.0.4 - detect-libc: 2.0.2 - optionalDependencies: - '@libsql/darwin-arm64': 0.5.29 - '@libsql/darwin-x64': 0.5.29 - '@libsql/linux-arm-gnueabihf': 0.5.29 - '@libsql/linux-arm-musleabihf': 0.5.29 - '@libsql/linux-arm64-gnu': 0.5.29 - '@libsql/linux-arm64-musl': 0.5.29 - '@libsql/linux-x64-gnu': 0.5.29 - '@libsql/linux-x64-musl': 0.5.29 - '@libsql/win32-x64-msvc': 0.5.29 - lightningcss-android-arm64@1.32.0: optional: true @@ -2295,10 +1774,6 @@ snapshots: locate-character@3.0.0: {} - lodash.defaults@4.2.0: {} - - lodash.isarguments@3.1.0: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2324,8 +1799,6 @@ snapshots: mrmime@2.0.1: {} - ms@2.1.3: {} - nanoid@3.3.11: {} napi-build-utils@2.0.0: {} @@ -2334,8 +1807,6 @@ snapshots: dependencies: semver: 7.7.4 - nodemailer@8.0.7: {} - obug@2.1.1: {} once@1.4.0: @@ -2382,8 +1853,6 @@ snapshots: prettier@3.8.3: {} - promise-limit@2.7.0: {} - pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -2408,12 +1877,6 @@ snapshots: readdirp@5.0.0: {} - redis-errors@1.2.0: {} - - redis-parser@3.0.0: - dependencies: - redis-errors: 1.2.0 - resolve-pkg-maps@1.0.0: {} rolldown@1.0.1: @@ -2470,10 +1933,6 @@ snapshots: sprintf-js@1.0.3: {} - sql-template-tag@5.2.1: {} - - standard-as-callback@2.1.0: {} - string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -2539,8 +1998,6 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - toad-cache@3.7.0: {} - totalist@3.0.1: {} tslib@2.8.1: @@ -2563,10 +2020,6 @@ snapshots: undici-types@7.19.2: {} - universal-github-app-jwt@2.2.2: {} - - universal-user-agent@7.0.3: {} - util-deprecate@1.0.2: {} vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0)(yaml@2.8.4): @@ -2589,8 +2042,6 @@ snapshots: wrappy@1.0.2: {} - ws@8.20.0: {} - yaml@2.8.4: {} zimmerframe@1.1.4: {} diff --git a/scripts/env.check.ts b/scripts/env.check.ts deleted file mode 100644 index bfe4550be..000000000 --- a/scripts/env.check.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { readFileSync } from 'node:fs' - -if (process.env.CI) { - process.exit(0) -} - -function get_environment_variables(path: string): string[] { - let file = '' - try { - file = readFileSync(path, 'utf8') - } catch (_) { - console.error(`❌ ${path} does not exist. Please create it first.`) - process.exit(1) - } - - return file - .split('\n') - .map((line) => line.trim()) - .filter((line) => line !== '' && !line.startsWith('#')) - .map((line) => line.split('=', 1)[0].trim()) -} - -const actual_variables = get_environment_variables('.env') -const example_variables = get_environment_variables('.env.example') - -if (!actual_variables.length) { - console.error( - '❌ .env does not contain any environment variables. Please update it from .env.example.' - ) - process.exit(1) -} - -const are_the_same = - actual_variables.length === example_variables.length && - example_variables.every((v) => actual_variables.includes(v)) - -if (are_the_same) { - console.info('✅ Environment variables are up to date.') -} else { - console.error('❌ .env is out of date. Please update it to match .env.example.') - process.exit(1) -} diff --git a/src/components/SuggestionForm.svelte b/src/components/SuggestionForm.svelte index 5bb36f7d0..b5ac1f2eb 100644 --- a/src/components/SuggestionForm.svelte +++ b/src/components/SuggestionForm.svelte @@ -1,6 +1,7 @@ - - - -

Admin Page

- - diff --git a/src/routes/admin/login/+page.server.ts b/src/routes/admin/login/+page.server.ts deleted file mode 100644 index 04f4dc70b..000000000 --- a/src/routes/admin/login/+page.server.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ADMIN_PAGE_PASSWORD } from '$env/static/private' -import { fail, redirect } from '@sveltejs/kit' -import type { Actions } from './$types' -import { create_session } from '../sessions' -import { redis } from '$lib/server/redis' - -export const prerender = false - -export const actions: Actions = { - login: async (event) => { - const ip = event.getClientAddress() - const key = `rate_limit_admin_login:ip:${ip}` - const count = Number((await redis.get(key)) ?? 0) - - if (count >= 5) { - return fail(429, { - error: 'Too many login attempts. Please try again later.' - }) - } - - const form = await event.request.formData() - const password = form.get('password') - - if (!password || typeof password !== 'string') { - return fail(400, { error: 'Password required' }) - } - - if (password !== ADMIN_PAGE_PASSWORD) { - const next = await redis.incr(key) - if (next === 1) await redis.expire(key, 60 * 10) - - return fail(400, { error: 'Password incorrect' }) - } - - create_session(event) - - await redis.del(key) - - redirect(303, '/admin') - } -} diff --git a/src/routes/admin/login/+page.svelte b/src/routes/admin/login/+page.svelte deleted file mode 100644 index e5b605de8..000000000 --- a/src/routes/admin/login/+page.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - - - -

Admin Login

- -
-
- - -
-
- -
-
- -{#if form?.error} -

{form.error}

-{/if} diff --git a/src/routes/admin/sessions.ts b/src/routes/admin/sessions.ts deleted file mode 100644 index 5587124f1..000000000 --- a/src/routes/admin/sessions.ts +++ /dev/null @@ -1,24 +0,0 @@ -import crypto from 'node:crypto' -import type { RequestEvent } from '@sveltejs/kit' - -const sessions = new Set() - -const ADMIN_SESSION_COOKIE_NAME = 'admin_session_id' - -export function create_session(event: RequestEvent) { - const session_id = crypto.randomUUID() - sessions.add(session_id) - - event.cookies.set(ADMIN_SESSION_COOKIE_NAME, session_id, { - path: '/', - httpOnly: true, - secure: true, - sameSite: 'lax', - maxAge: 60 * 60 - }) -} - -export function has_session(event: RequestEvent) { - const session_id = event.cookies.get(ADMIN_SESSION_COOKIE_NAME) - return !!session_id && sessions.has(session_id) -} diff --git a/src/routes/admin/statistics/+page.server.ts b/src/routes/admin/statistics/+page.server.ts deleted file mode 100644 index b9e8b6d11..000000000 --- a/src/routes/admin/statistics/+page.server.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { batch_app } from '$lib/server/db.app' -import { error } from '@sveltejs/kit' -import sql from 'sql-template-tag' -import { has_session } from '../sessions' -import { redirect } from '@sveltejs/kit' - -export const prerender = false - -export const load = async (event) => { - if (!has_session(event)) redirect(307, '/admin/login') - - const { err, results } = await batch_app< - [ - { - start: string - total: number - total_last_day: number - total_last_week: number - total_last_month: number - }, - { - day: string - count: number - }, - { - country: string | null - count: number - }, - { - theme: string - count: number - percentage: number - }, - { - device_type: string - count: number - percentage: number - } - ] - >([ - sql`SELECT - COALESCE(MIN(created_at), '') AS start, - COUNT(*) AS total, - COUNT(CASE WHEN created_at >= datetime('now', '-1 day') THEN 1 END) AS total_last_day, - COUNT(CASE WHEN created_at >= datetime('now', '-7 days') THEN 1 END) AS total_last_week, - COUNT(CASE WHEN created_at >= datetime('now', '-1 month') THEN 1 END) AS total_last_month - FROM visits`, - sql`SELECT - date(created_at) AS day, - COUNT(*) AS count - FROM visits - WHERE created_at >= datetime('now', '-14 days') - GROUP BY day - ORDER BY day DESC - `, - sql`SELECT - country, - COUNT(*) AS count - FROM visits - GROUP BY country - ORDER BY count DESC - LIMIT 10 - `, - sql`SELECT - theme, - COUNT(*) AS count, - ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS percentage - FROM visits - GROUP BY theme - ORDER BY theme`, - sql`SELECT - device_type, - COUNT(*) AS count, - ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS percentage - FROM visits - GROUP BY device_type - ORDER BY count DESC` - ]) - - if (err) { - error(500, 'Failed to load statistics') - } - - const [ - [{ start, total, total_last_day, total_last_week, total_last_month }], - daily_visits, - country_stats, - theme_stats, - device_stats - ] = results - - return { - start, - total, - total_last_day, - total_last_week, - total_last_month, - daily_visits, - country_stats, - theme_stats, - device_stats - } -} diff --git a/src/routes/admin/statistics/+page.svelte b/src/routes/admin/statistics/+page.svelte deleted file mode 100644 index ecfe6d492..000000000 --- a/src/routes/admin/statistics/+page.svelte +++ /dev/null @@ -1,164 +0,0 @@ - - - - -

Back to the Admin Page

- -

Statistics

- -CatDat has been visited {data.total} times since its launch on -{data.start.substring(0, 10)}. There has been {data.total_last_day} -visits in the last day, {data.total_last_week} -visits in the last week, and {data.total_last_month} visits in the last -month. - -

Daily Visits – Last 2 Weeks

- - - - - - - - - - - - - - - {#each data.daily_visits as visit} - - - - - {/each} - -
Day#
- {visit.day} - - {visit.count} -
- -

Countries – Top 10

- - - - - - - - - - - - - - - {#each data.country_stats as stat} - - - - - {/each} - -
Country#
- {stat.country} - - {stat.count} -
- -

Device Types

- - - - - - - - - - - - - - - - - {#each data.device_stats as stat} - - - - - - {/each} - -
Device Types#%
- {stat.device_type} - - {stat.count} - - {stat.percentage}% -
- -

Themes

- - - - - - - - - - - - - - - - - {#each data.theme_stats as stat} - - - - - - {/each} - -
Themes#%
- {stat.theme} - - {stat.count} - - {stat.percentage}% -
- - diff --git a/src/routes/admin/submissions/+page.server.ts b/src/routes/admin/submissions/+page.server.ts deleted file mode 100644 index 8e8e0ac7c..000000000 --- a/src/routes/admin/submissions/+page.server.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { error, fail, redirect } from '@sveltejs/kit' -import { has_session } from '../sessions' -import { query_app } from '$lib/server/db.app' -import sql from 'sql-template-tag' -import { App } from '@octokit/app' -import { GITHUB_PRIVATE_KEY } from '$env/static/private' - -export const prerender = false - -const GITHUB_APP_ID = '3330448' -const GITHUB_INSTALLATION_ID = '122747163' -const GITHUB_OWNER = 'ScriptRaccoon' -const GITHUB_REPO = 'CatDat' - -export const load = async (event) => { - if (!has_session(event)) redirect(307, '/admin/login') - - const { rows: submissions, err } = await query_app<{ - id: number - title: string - body: string - url: string - name: string | null - created_at: string - approved_at: string | null - }>(sql` - SELECT id, title, body, url, name, created_at, approved_at - FROM submissions - ORDER BY created_at DESC - `) - - if (err) { - error(500, 'Could not load submissions') - } - - return { submissions } -} - -export const actions = { - delete: async (event) => { - if (!has_session(event)) redirect(307, '/admin/login') - - const form = await event.request.formData() - const submission_id = form.get('id') - if (!submission_id) return fail(400, { error: 'Submission ID required' }) - - const { err } = await query_app( - sql`DELETE FROM submissions WHERE id = ${submission_id}` - ) - - if (err) return fail(500, { error: 'Failed to delete submission' }) - }, - - approve: async (event) => { - if (!has_session(event)) redirect(307, '/admin/login') - - if (!GITHUB_PRIVATE_KEY) return fail(401, { error: 'Unauthorized' }) - - const form = await event.request.formData() - const submission_id = form.get('id') - if (!submission_id) return fail(400, { error: 'Submission ID required' }) - - const { rows, err } = await query_app<{ - title: string - body: string - name: string | null - url: string - }>( - sql` - UPDATE submissions - SET approved_at = CURRENT_TIMESTAMP - WHERE id = ${submission_id} - RETURNING title, body, name, url - ` - ) - - if (err) return fail(500, { error: 'Failed to approve submission' }) - - const { title, body, name, url } = rows[0] - - const app = new App({ - appId: GITHUB_APP_ID, - privateKey: GITHUB_PRIVATE_KEY - }) - - const footer = name - ? `This issue has been created by **${name}** via the submission form on ${url}` - : `This issue has been created via the submission form on ${url}` - - const full_body = `${body}\n\n---\n${footer}` - - try { - const octokit = await app.getInstallationOctokit( - Number(GITHUB_INSTALLATION_ID) - ) - - const issue = await octokit.request('POST /repos/{owner}/{repo}/issues', { - owner: GITHUB_OWNER, - repo: GITHUB_REPO, - title, - body: full_body - }) - - return { issue_url: issue.data.html_url } - } catch (err) { - console.error(err) - return fail(502, { error: 'Issue could not be created' }) - } - } -} diff --git a/src/routes/admin/submissions/+page.svelte b/src/routes/admin/submissions/+page.svelte deleted file mode 100644 index fed28b1d4..000000000 --- a/src/routes/admin/submissions/+page.svelte +++ /dev/null @@ -1,122 +0,0 @@ - - - - -

Back to the Admin Page

- -

Submissions

- -

- Approve submissions sent by the suggestion form to convert them to GitHub issues. -

- -{#if form?.error} -

{form.error}

-{:else if form?.issue_url} -

- - The GitHub issue has been created. -

-{/if} - -{#if data.submissions.length} - {#each data.submissions as submission (submission.id)} -
-

- - - {submission.title} -

-
{submission.body}
- -
    - {#if submission.name} -
  • - - {submission.name} -
  • - {/if} - -
  • - - {new Date(submission.created_at).toLocaleString()} -
  • - - {#if submission.approved_at} -
  • - - {new Date(submission.approved_at).toLocaleString()} -
  • - {/if} - -
  • - - {submission.url} -
  • -
- -
- - {#if !submission.approved_at} - - {/if} - -
-
- {/each} -{:else} -

No submissions found

-{/if} - - diff --git a/src/routes/api/submissions/+server.ts b/src/routes/api/submissions/+server.ts deleted file mode 100644 index d22e8d06f..000000000 --- a/src/routes/api/submissions/+server.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { json } from '@sveltejs/kit' -import sql from 'sql-template-tag' -import { redis } from '$lib/server/redis' -import { send_email } from '$lib/server/email' -import { APPROVAL_EMAIL } from '$env/static/private' -import { query_app } from '$lib/server/db.app' - -const TITLE_MAX_LENGTH = 50 -const BODY_MAX_LENGTH = 10000 -const NAME_MAX_LENGTH = 50 - -export const POST = async (event) => { - const ip = event.getClientAddress() - const key = `rate_limit_submissions:ip:${ip}` - const count = await redis.incr(key) - if (count === 1) await redis.expire(key, 60 * 30) - - if (count > 5) { - return json( - { error: 'Too many submissions. Please try again later.' }, - { status: 429 } - ) - } - - const data = await parse_data(event.request) - - if ('error' in data) return json({ error: data.error }, { status: 400 }) - - const { title, body, url, name } = data - - const { err } = await query_app(sql` - INSERT INTO submissions (title, body, url, name) - VALUES (${title}, ${body}, ${url}, ${name}) - `) - - if (err) { - return json({ error: 'Internal Server Error' }, { status: 500 }) - } - - const approve_url = `${event.url.origin}/admin/submissions` - - const email_text = - `The suggestion form has been submitted.\n\n` + - `Title: ${title}\n\n` + - `Approve it here: ${approve_url}` - - try { - await send_email({ - subject: 'CatDat – New submission', - text: email_text, - to: APPROVAL_EMAIL - }) - } catch (err) { - console.error(err) - } - - return json({ message: 'Submission successful' }) -} - -async function parse_data(request: Request): Promise< - | { error: string } - | { - title: string - body: string - url: string - name: string | null - } -> { - const content_type = request.headers.get('Content-Type') - if (content_type !== 'application/json') { - return { error: 'Forbidden' } - } - - let data - try { - data = await request.json() - } catch (_) { - return { error: 'Invalid request body' } - } - - const { title, body, url, name = '' } = data - - if (!title) return { error: 'Title required' } - if (!body) return { error: 'Body required' } - if (!url) return { error: 'URL required' } - - if (typeof title !== 'string') return { error: 'Title must be a string' } - if (typeof body !== 'string') return { error: 'Body must be a string' } - if (typeof url !== 'string') return { error: 'URL must be a string' } - if (typeof name !== 'string') return { error: 'Name must be a string' } - - if (title.length > TITLE_MAX_LENGTH) { - return { error: `Title must have at most ${TITLE_MAX_LENGTH} characters` } - } - if (body.length > BODY_MAX_LENGTH) { - return { error: `Body must have at most ${BODY_MAX_LENGTH} characters` } - } - - if (!url.startsWith('https://') && !url.startsWith('http://')) { - return { error: 'URL must be a valid URL' } - } - - if (name.length > NAME_MAX_LENGTH) { - return { error: `Name must have at most ${NAME_MAX_LENGTH} characters` } - } - - return { title, body, url, name: name || null } -} diff --git a/src/routes/api/track/+server.ts b/src/routes/api/track/+server.ts deleted file mode 100644 index 7126b8a64..000000000 --- a/src/routes/api/track/+server.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { is_object } from '$shared/utils' -import { json } from '@sveltejs/kit' -import { get_geo_data, is_allowed } from './track.utils' -import { query_app } from '$lib/server/db.app' -import sql from 'sql-template-tag' - -type ValidBody = { device_type: string; theme: string } - -const is_valid_body = (body: unknown): body is ValidBody => - is_object(body) && - 'device_type' in body && - typeof body.device_type === 'string' && - 'theme' in body && - typeof body.theme === 'string' - -export const POST = async (event) => { - if (!is_allowed(event)) return json({ error: 'Forbidden' }, { status: 403 }) - - if (event.request.headers.get('Content-Type') !== 'application/json') { - return json({ error: 'Invalid Request' }, { status: 400 }) - } - - const body: unknown = await event.request.json() - - if (!is_valid_body(body)) { - return json({ error: 'Invalid Request Body' }, { status: 400 }) - } - - const { device_type, theme } = body - - const { country } = get_geo_data(event.request) - - const { err } = await query_app(sql` - INSERT INTO visits (theme, device_type, country) - VALUES (${theme}, ${device_type}, ${country}) - `) - - if (err) return json({ error: 'Database error' }, { status: 500 }) - - return json({ message: 'Visit has been tracked' }) -} diff --git a/src/routes/api/track/track.utils.ts b/src/routes/api/track/track.utils.ts deleted file mode 100644 index 47184397e..000000000 --- a/src/routes/api/track/track.utils.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Crawler } from 'es6-crawler-detect' -import type { RequestEvent } from '@sveltejs/kit' - -const crawler_detector = new Crawler() - -function is_bot(request: Request): boolean { - const user_agent = request.headers.get('user-agent') ?? '' - return crawler_detector.isCrawler(user_agent) -} - -function is_same_origin(request: Request, site_origin: string): boolean { - const origin = request.headers.get('origin') - const referer = request.headers.get('referer') - - if (origin !== null) return origin === site_origin - if (referer !== null) return referer.startsWith(site_origin) - - return false -} - -export function is_allowed(event: RequestEvent): boolean { - return is_same_origin(event.request, event.url.origin) && !is_bot(event.request) -} - -export function get_geo_data(request: Request): { - country: string | null -} { - // Netlify specific geo header - const geo_header = request.headers.get('x-nf-geo') - - if (!geo_header) return { country: null } - - try { - const txt = decode_base64_utf8(geo_header) - const decoded = JSON.parse(txt) - const country = decoded.country?.name || null - return { country } - } catch (_) { - console.error('Netlify geo header cannot be parsed:', geo_header) - return { country: null } - } -} - -function decode_base64_utf8(input: string): string { - const bytes = Uint8Array.from(atob(input), (c) => c.charCodeAt(0)) - return new TextDecoder('utf-8').decode(bytes) -} diff --git a/src/routes/api/user_action/+server.ts b/src/routes/api/user_action/+server.ts deleted file mode 100644 index 7ffe9b48f..000000000 --- a/src/routes/api/user_action/+server.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { query_app } from '$lib/server/db.app' -import { is_object } from '$shared/utils' -import sql from 'sql-template-tag' -import { is_allowed } from '../track/track.utils' -import { json } from '@sveltejs/kit' - -type ValidBody = { action: string; value?: string } - -const is_valid_body = (body: unknown): body is ValidBody => - is_object(body) && - 'action' in body && - typeof body.action === 'string' && - (!('value' in body) || typeof body.value === 'string') - -export const POST = async (event) => { - if (!is_allowed(event)) return json({ error: 'Forbidden' }, { status: 403 }) - - if (event.request.headers.get('Content-Type') !== 'application/json') { - return json({ error: 'Invalid Request' }, { status: 400 }) - } - - const body: unknown = await event.request.json() - - if (!is_valid_body(body)) { - return json({ error: 'Invalid Request Body' }, { status: 400 }) - } - - const { action, value = null } = body - - const { err } = await query_app( - sql`INSERT INTO user_actions (action, value) VALUES (${action}, ${value})` - ) - - if (err) return json({ error: 'Database error' }, { status: 500 }) - - return json({ message: 'User action has been recorded' }) -} diff --git a/src/routes/download/+page.svelte b/src/routes/download/+page.svelte index 7a5026d14..0713883d6 100644 --- a/src/routes/download/+page.svelte +++ b/src/routes/download/+page.svelte @@ -1,11 +1,12 @@