From 8b8ace0c963ef77ae7cba85d93afb4e61db640dd Mon Sep 17 00:00:00 2001 From: Script Raccoon Date: Sat, 11 Jul 2026 22:36:37 +0200 Subject: [PATCH 1/6] replace internal with external API endpoints --- src/components/SuggestionForm.svelte | 3 +- src/lib/client/track.ts | 3 +- src/lib/server/email.ts | 41 ---------- src/routes/api/submissions/+server.ts | 108 -------------------------- src/routes/api/track/+server.ts | 41 ---------- src/routes/api/track/track.utils.ts | 47 ----------- src/routes/api/user_action/+server.ts | 37 --------- src/routes/download/+page.svelte | 3 +- 8 files changed, 6 insertions(+), 277 deletions(-) delete mode 100644 src/lib/server/email.ts delete mode 100644 src/routes/api/submissions/+server.ts delete mode 100644 src/routes/api/track/+server.ts delete mode 100644 src/routes/api/track/track.utils.ts delete mode 100644 src/routes/api/user_action/+server.ts 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/static/robots.txt b/static/robots.txt index 4cc3fa229..b6dd6670c 100644 --- a/static/robots.txt +++ b/static/robots.txt @@ -1,3 +1,3 @@ # allow crawling everything by default User-agent: * -Disallow: /admin +Disallow: From 70f93022b4505199383926ba2364ebbee30017f6 Mon Sep 17 00:00:00 2001 From: Script Raccoon Date: Sun, 12 Jul 2026 00:19:02 +0200 Subject: [PATCH 3/6] fix playwright in test workflow --- .github/workflows/test.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3af0bbb9d..7693582d0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -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 From fbbc6df0fbdcdeacba6bd948aeff70d5ed31b843 Mon Sep 17 00:00:00 2001 From: Script Raccoon Date: Sun, 12 Jul 2026 00:31:52 +0200 Subject: [PATCH 4/6] add .env to git since only non-sensitive variables are left --- .env | 3 +++ .env.example | 5 ----- .gitignore | 5 ----- CONTRIBUTING.md | 7 +++---- README.md | 7 +++---- package.json | 3 +-- scripts/env.check.ts | 42 ------------------------------------------ 7 files changed, 10 insertions(+), 62 deletions(-) create mode 100644 .env delete mode 100644 .env.example delete mode 100644 scripts/env.check.ts 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 b40c5a522..000000000 --- a/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# When cloning the repository, copy this file as .env -# You don't need to change any values. - -PUBLIC_ADMIN_URL=http://localhost:5174 -PUBLIC_PLAYWRIGHT= \ No newline at end of file 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 5d0cbe504..a3f4accc3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,10 +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`. +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/README.md b/README.md index 1064296d2..2c5df0a72 100644 --- a/README.md +++ b/README.md @@ -43,10 +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`. +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 diff --git a/package.json b/package.json index a2bf62630..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,7 +14,6 @@ "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": "pnpm db:kill && tsx --tsconfig databases/tsconfig.json databases/catdat/scripts/setup.ts", 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) -} From 36fcdde1eaaacf7eb12940c300ce468871ffef54 Mon Sep 17 00:00:00 2001 From: Script Raccoon Date: Sun, 12 Jul 2026 00:48:05 +0200 Subject: [PATCH 5/6] add link to admin repo --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 2c5df0a72..016120abf 100644 --- a/README.md +++ b/README.md @@ -58,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: From 8e9ee2d9a452cee65050795efc9585072b23c0a0 Mon Sep 17 00:00:00 2001 From: Script Raccoon Date: Sun, 12 Jul 2026 01:04:15 +0200 Subject: [PATCH 6/6] admin becomes becomes variable since it is not a secret --- .github/workflows/_deploy-reusable.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/_deploy-reusable.yaml b/.github/workflows/_deploy-reusable.yaml index d4e091cda..929ac5ac1 100644 --- a/.github/workflows/_deploy-reusable.yaml +++ b/.github/workflows/_deploy-reusable.yaml @@ -11,8 +11,6 @@ on: required: true NETLIFY_ACCESS_TOKEN: required: true - PUBLIC_ADMIN_URL: - required: true jobs: build-and-deploy: @@ -47,7 +45,7 @@ jobs: - name: Build app env: PUBLIC_PLAYWRIGHT: '' - PUBLIC_ADMIN_URL: ${{ secrets.PUBLIC_ADMIN_URL }} + PUBLIC_ADMIN_URL: ${{ vars.PUBLIC_ADMIN_URL }} run: pnpm build - name: Get latest commit message