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.
-
-## 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
-
-
-
-
-
-
-
-
-
- | Day |
- # |
-
-
-
- {#each data.daily_visits as visit}
-
- |
- {visit.day}
- |
-
- {visit.count}
- |
-
- {/each}
-
-
-
-Countries – Top 10
-
-
-
-
-
-
-
-
-
- | Country |
- # |
-
-
-
- {#each data.country_stats as stat}
-
- |
- {stat.country}
- |
-
- {stat.count}
- |
-
- {/each}
-
-
-
-Device Types
-
-
-
-
-
-
-
-
-
-
- | Device Types |
- # |
- % |
-
-
-
- {#each data.device_stats as stat}
-
- |
- {stat.device_type}
- |
-
- {stat.count}
- |
-
- {stat.percentage}%
- |
-
- {/each}
-
-
-
-Themes
-
-
-
-
-
-
-
-
-
-
- | Themes |
- # |
- % |
-
-
-
- {#each data.theme_stats as stat}
-
- |
- {stat.theme}
- |
-
- {stat.count}
- |
-
- {stat.percentage}%
- |
-
- {/each}
-
-
-
-
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}
-
-
-
-
-
- {/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 @@