init: add code

This commit is contained in:
Xory 2026-01-30 17:03:55 +02:00
commit 79c4323666
26 changed files with 3684 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use_nix

35
.gitignore vendored Normal file
View file

@ -0,0 +1,35 @@
# prod
dist/
# dev
.yarn/
!.yarn/releases
.vscode/*
!.vscode/launch.json
!.vscode/*.code-snippets
.idea/workspace.xml
.idea/usage.statistics.xml
.idea/shelf
# deps
node_modules/
.wrangler
# env
.env
.env.production
.dev.vars
# logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# misc
.DS_Store
/src/generated/prisma

12
README.md Normal file
View file

@ -0,0 +1,12 @@
# anum
the unholy lovechild of Xitter and 4chan
## migration tutorial because i have dementia
### if this makes it to prod im going to follow lowtiergod's advice
1. `cp prisma/schema.prisma prisma/schema.prisma.old`
2. touch schema.prisma like diddy touches children
3. tell wrangler that we are Migrationing It
4. get prisma to create a migration from the old ass motherfucker schema to the new babyoiled one and throw its output in wrangler's migration template
5. tell wrangler that we have Migrationed It go apply this shit now you stinky ass motherfucker blackout revival flare looking ass
6. `rm prisma/schema.prisma.old`

166
ai_seed.sql Normal file
View file

@ -0,0 +1,166 @@
BEGIN TRANSACTION;
-- 1. CLEANUP (Optional: Remove if you want to keep existing data)
DELETE FROM "Post";
DELETE FROM "User";
DELETE FROM "sqlite_sequence"; -- Resets autoincrement counters
-- 2. CREATE USERS
-- (Passwords are all the same dummy bcrypt hash for "password123")
INSERT INTO "User" (id, email, name, display_name, password) VALUES
(1, 'admin@forum.com', 'admin', 'SysAdmin', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
(2, 'steve@moderator.com', 'mod_steve', 'Steve [MOD]', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
(3, 'jane.doe@gmail.com', 'janedoe', 'Jane D.', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
(4, 'rust_evangelist@tech.com', 'rusty', 'RustInPeace', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
(5, 'cpp_veteran@legacy.com', 'segfault', 'C++ Grandmaster', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
(6, 'troll123@hotmail.com', 'u_mad_bro', 'Anonymous', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
(7, 'helpme@student.edu', 'noob_coder', 'HelpMePls', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
(8, 'linux_fan@gnu.org', 'sudo_make_me', 'Arch User', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
(9, 'webdev@agency.com', 'div_center', 'CSS Wizard', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
(10, 'gamer@twitch.tv', 'xx_sniper_xx', 'Pro Gamer', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
(11, 'bot@bot.com', 'news_bot', 'News Aggregator', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
(12, 'lurker@yahoo.com', 'lurker007', NULL, '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
(13, 'vim@editor.com', 'vim_enthusiast', ':wq', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
(14, 'emacs@editor.com', 'emacs_guru', 'M-x Butterfly', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
(15, 'react@fb.com', 'hook_master', 'UseEffect', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7');
-- 3. CREATE THREADS (Top level posts)
-- We manually assign IDs here to ensure the replies attach correctly later.
-- Thread 1: Welcome (Sticky)
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
(1, 'Welcome to the Community! READ FIRST', 'Welcome everyone. Please be nice. No spamming. Enjoy your stay!', 1, NULL, datetime('now', '-30 days'));
-- Thread 2: Flame war (Tabs vs Spaces)
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
(2, 'Unpopular Opinion: Spaces are objectively better than Tabs', 'I said what I said. If you use tabs, your formatting is at the mercy of the editor settings. Discuss.', 3, NULL, datetime('now', '-15 days'));
-- Thread 3: Tech Support
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
(3, 'URGENT: Cannot exit Vim?', 'I have been stuck in Vim for 4 days. I have tried Esc, Ctrl+C, Alt+F4. I am starving. Please help.', 7, NULL, datetime('now', '-2 days'));
-- Thread 4: Gaming
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
(4, 'Elden Ring DLC discussion', 'Has anyone beaten the final boss yet? No spoilers please, but that second phase is brutal.', 10, NULL, datetime('now', '-5 days'));
-- Thread 5: Programming Language War
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
(5, 'Why Rust will replace C++ in 5 years', 'Memory safety is not optional anymore. The borrow checker is your friend.', 4, NULL, datetime('now', '-10 days'));
-- Thread 6: Off Topic Megathread (This will house most of the "volume")
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
(6, 'MEGATHREAD: What are you listening to right now?', 'Post your current jam. Keep it clean.', 2, NULL, datetime('now', '-60 days'));
-- Thread 7: Random
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
(7, 'My cat walked on my keyboard and deployed to production', 'Fortunately, the tests passed. I think my cat is a better dev than me.', 9, NULL, datetime('now', '-1 day'));
-- Thread 8: News
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
(8, 'Tech News: AI takes over toaster industry', 'Smart toasters are now requiring a subscription to toast bagels.', 11, NULL, datetime('now', '-20 days'));
-- 4. CREATE REALISTIC REPLIES
-- Thread 1 (Welcome) Replies
INSERT INTO "Post" (title, content, userId, parentId, createdAt) VALUES
('Re: Welcome', 'Thanks admin! Glad to be here.', 3, 1, datetime('now', '-29 days')),
('Re: Welcome', 'First!', 6, 1, datetime('now', '-29 days')),
('Re: Welcome', 'Please do not post "First", this is not YouTube.', 2, 1, datetime('now', '-29 days')),
('Re: Welcome', 'Hello world.', 5, 1, datetime('now', '-28 days'));
-- Thread 2 (Tabs vs Spaces) - The Fight
INSERT INTO "Post" (title, content, userId, parentId, createdAt) VALUES
('Re: Tabs vs Spaces', 'You are wrong. Tabs save file size. Imagine a 1MB file, tabs save bytes!', 5, 2, datetime('now', '-14 days')),
('Re: Tabs vs Spaces', 'Storage is cheap. Consistency is priceless. Spaces all the way.', 9, 2, datetime('now', '-14 days')),
('Re: Tabs vs Spaces', 'I use a mix of both just to annoy my coworkers.', 6, 2, datetime('now', '-14 days')),
('Re: Tabs vs Spaces', 'You monster.', 3, 2, datetime('now', '-13 days')),
('Re: Tabs vs Spaces', 'Go style guide says tabs. I follow the Go style guide.', 4, 2, datetime('now', '-13 days')),
('Re: Tabs vs Spaces', 'Python enforces indentation. Spaces are the standard there.', 8, 2, datetime('now', '-13 days'));
-- Thread 3 (Vim Help)
INSERT INTO "Post" (title, content, userId, parentId, createdAt) VALUES
('Re: Vim', 'Type :q! and hit enter.', 13, 3, datetime('now', '-2 days')),
('Re: Vim', 'Or just unplug your computer.', 6, 3, datetime('now', '-2 days')),
('Re: Vim', 'Just switch to Emacs, we have a menu bar.', 14, 3, datetime('now', '-2 days')),
('Re: Vim', 'OMG THANK YOU @vim_enthusiast you saved my life.', 7, 3, datetime('now', '-1 day'));
-- Thread 5 (Rust vs C++)
INSERT INTO "Post" (title, content, userId, parentId, createdAt) VALUES
('Re: Rust', 'C++ is still faster if you know what you are doing.', 5, 5, datetime('now', '-9 days')),
('Re: Rust', 'Most people do not know what they are doing though.', 4, 5, datetime('now', '-9 days')),
('Re: Rust', 'Rewriting everything in Rust is not feasible for large legacy codebases.', 1, 5, datetime('now', '-8 days')),
('Re: Rust', 'Agreed, but for greenfield projects it is a no brainer.', 9, 5, datetime('now', '-8 days')),
('Re: Rust', 'I like Zig.', 8, 5, datetime('now', '-7 days'));
-- Thread 7 (Cat deployment)
INSERT INTO "Post" (title, content, userId, parentId, createdAt) VALUES
('Re: Cat', 'Is the cat looking for a job? We are hiring.', 1, 7, datetime('now', '-23 hours')),
('Re: Cat', 'Pics or it didnt happen.', 6, 7, datetime('now', '-20 hours')),
('Re: Cat', 'Mine just sleeps on the warm laptop vent.', 15, 7, datetime('now', '-10 hours'));
-- 5. GENERATE BULK VOLUME (The "Megathread")
-- We use a recursive query or just a massive block of inserts to hit ~500 posts.
-- We will simulate a "What are you listening to" thread (Parent ID 6).
INSERT INTO "Post" (title, content, userId, parentId, createdAt) VALUES
('Song', 'Pink Floyd - Echoes', 8, 6, datetime('now', '-59 days')),
('Song', 'Daft Punk - Discovery (Whole Album)', 9, 6, datetime('now', '-58 days')),
('Song', 'Lo-fi beats to study to', 7, 6, datetime('now', '-58 days')),
('Song', 'Metallica - Master of Puppets', 5, 6, datetime('now', '-57 days')),
('Song', 'Darude - Sandstorm', 6, 6, datetime('now', '-56 days')),
('Song', 'Rick Astley - Never Gonna Give You Up', 6, 6, datetime('now', '-56 days')),
('Song', 'Bach - Cello Suite No. 1', 3, 6, datetime('now', '-55 days')),
('Song', 'Kendrick Lamar - DNA', 15, 6, datetime('now', '-54 days')),
('Song', 'Tool - Lateralus', 4, 6, datetime('now', '-53 days')),
('Song', 'Taylor Swift', 10, 6, datetime('now', '-52 days')),
('Song', '+1 for Tool', 8, 6, datetime('now', '-52 days')),
('Song', 'Radiohead - OK Computer', 2, 6, datetime('now', '-50 days')),
('Song', 'Video Game OSTs mostly', 10, 6, datetime('now', '-49 days')),
('Song', 'Aphex Twin', 11, 6, datetime('now', '-48 days')),
('Song', 'Silence. My fans are too loud.', 12, 6, datetime('now', '-47 days')),
('Song', 'Jazz vibes', 14, 6, datetime('now', '-46 days'));
-- To get to 500 without writing 500 lines manually, we will use a cross join trick
-- to multiply these generic comments into the megathread (ID 6).
INSERT INTO "Post" (title, content, userId, parentId, createdAt)
SELECT
'Re: Music',
'Listening to track #' || (ABS(RANDOM()) % 1000),
(ABS(RANDOM()) % 15) + 1, -- Random User ID 1-15
6, -- Parent ID 6 (Music Thread)
datetime('now', '-' || (ABS(RANDOM()) % 60) || ' days')
FROM "User" u1, "User" u2, "User" u3
LIMIT 300; -- Adds 300 generic posts
-- Add some random replies to the Rust vs C++ thread (ID 5) to make it look heated
INSERT INTO "Post" (title, content, userId, parentId, createdAt)
SELECT
'Re: Rust vs C++',
CASE (ABS(RANDOM()) % 5)
WHEN 0 THEN 'I disagree strongly.'
WHEN 1 THEN 'This is the way.'
WHEN 2 THEN 'Have you read the documentation?'
WHEN 3 THEN 'Benchmarks prove otherwise.'
ELSE 'Interesting point.'
END,
(ABS(RANDOM()) % 15) + 1,
5, -- Parent ID 5
datetime('now', '-' || (ABS(RANDOM()) % 10) || ' days')
FROM "User" u1, "User" u2
LIMIT 50;
-- Add some "Bump" posts to random threads
INSERT INTO "Post" (title, content, userId, parentId, createdAt)
SELECT
'Bump',
'Bumping this thread.',
(ABS(RANDOM()) % 15) + 1,
(ABS(RANDOM()) % 8) + 1, -- Random Parent ID 1-8
datetime('now', '-1 hour')
FROM "User"
LIMIT 20;
COMMIT;

9
eslint.config.ts Normal file
View file

@ -0,0 +1,9 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import { defineConfig } from "eslint/config";
export default defineConfig([
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } },
tseslint.configs.recommended,
]);

59
flake.lock generated Normal file
View file

@ -0,0 +1,59 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1767364772,
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

15
flake.nix Normal file
View file

@ -0,0 +1,15 @@
{
description = "anum backend";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem
(system:
let pkgs = nixpkgs.legacyPackages.${system}; in
{
devShells.default = import ./shell.nix { inherit pkgs; };
}
);
}

23
migrations/0001_init.sql Normal file
View file

@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"display_name" TEXT,
"password" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "Post" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "User_name_key" ON "User"("name");

View file

@ -0,0 +1,17 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Post" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"parentId" INTEGER,
CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Post_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Post" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_Post" ("content", "id", "title", "userId") SELECT "content", "id", "title", "userId" FROM "Post";
DROP TABLE "Post";
ALTER TABLE "new_Post" RENAME TO "Post";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View file

@ -0,0 +1,18 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Post" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"parentId" INTEGER,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Post_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Post" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_Post" ("content", "id", "parentId", "title", "userId") SELECT "content", "id", "parentId", "title", "userId" FROM "Post";
DROP TABLE "Post";
ALTER TABLE "new_Post" RENAME TO "Post";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View file

@ -0,0 +1 @@
provider = "sqlite"

28
package.json Normal file
View file

@ -0,0 +1,28 @@
{
"name": "anum",
"type": "module",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy --minify",
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
},
"dependencies": {
"@hono/zod-validator": "^0.7.6",
"@prisma/adapter-d1": "^7.2.0",
"@prisma/client": "^7.2.0",
"bcrypt-ts": "^8.0.0",
"hono": "^4.11.3",
"jose": "^6.1.3",
"zod": "^4.3.5"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"eslint": "^9.39.2",
"globals": "^17.0.0",
"jiti": "^2.6.1",
"prisma": "^7.2.0",
"typescript-eslint": "^8.51.0",
"vscode-langservers-extracted": "^4.10.0",
"wrangler": "^4.4.0"
}
}

2782
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

14
prisma.config.ts Normal file
View file

@ -0,0 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});

31
prisma/schema.prisma Normal file
View file

@ -0,0 +1,31 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String @unique
display_name String?
password String
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String
author User @relation(fields: [userId], references: [id])
userId Int
replies Post[] @relation("PostReplies")
parent Post? @relation("PostReplies", fields: [parentId], references: [id])
parentId Int?
createdAt DateTime @default(now())
}

48
seed.sql Normal file
View file

@ -0,0 +1,48 @@
-- 1. Generate 100 Users
-- Password is a bcrypt hash for "password123"
INSERT INTO "User" (email, name, display_name, password)
WITH RECURSIVE
cnt(n) AS (
SELECT 1
UNION ALL
SELECT n+1 FROM cnt WHERE n < 100
)
SELECT
'user' || n || '@example.com',
'user_' || n,
'User ' || n,
'$2b$10$EpjVIByL7WqitL96.t65he1usN8L6j0r6n/1q7R8U6O1Y2v6y/L2.' -- password123
FROM cnt;
-- 2. Generate 700 Parent Posts
-- Distributed across the 100 users
INSERT INTO "Post" (title, content, userId, createdAt)
WITH RECURSIVE
cnt(n) AS (
SELECT 1
UNION ALL
SELECT n+1 FROM cnt WHERE n < 700
)
SELECT
'Post Title #' || n,
'This is the body content for sample post number ' || n || '. It contains some placeholder text for testing purposes.',
(ABS(RANDOM()) % 100) + 1, -- Random userId between 1 and 100
datetime('now', '-' || (ABS(RANDOM()) % 365) || ' days') -- Random date in the last year
FROM cnt;
-- 3. Generate 200 Replies
-- Linked to random parent posts (IDs 1-700)
INSERT INTO "Post" (title, content, userId, parentId, createdAt)
WITH RECURSIVE
cnt(n) AS (
SELECT 1
UNION ALL
SELECT n+1 FROM cnt WHERE n < 200
)
SELECT
'Reply #' || n,
'I am replying to an existing post to test the self-relation logic.',
(ABS(RANDOM()) % 100) + 1, -- Random author
(ABS(RANDOM()) % 700) + 1, -- Random parent post ID
datetime('now', '-' || (ABS(RANDOM()) % 30) || ' days') -- Random date in last 30 days
FROM cnt;

15
shell.nix Normal file
View file

@ -0,0 +1,15 @@
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
buildInputs = with pkgs; [
prisma_7
pnpm_10
nodejs_25
vscode-langservers-extracted
typescript
];
shellHook = ''
export PRISMA_QUERY_ENGINE_LIBRARY=${pkgs.prisma-engines}/lib/libquery_engine.node
export PRISMA_QUERY_ENGINE_BINARY=${pkgs.prisma-engines}/bin/query-engine
export PRISMA_SCHEMA_ENGINE_BINARY=${pkgs.prisma-engines}/bin/schema-engine
'';
}

49
shell.nix.bak Normal file
View file

@ -0,0 +1,49 @@
{ pkgs ? import <nixpkgs> {} }:
(pkgs.buildFHSEnv {
name = "prisma-v7-shell";
targetPkgs = pkgs: (with pkgs; [
nodejs
pnpm
openssl
zlib
]);
runScript = "bash";
profile = ''
# 1. Define the correct version and commit for Prisma 7.2.0
PRISMA_VERSION="7.2.0"
COMMIT="0c8ef2ce45c83248ab3df073180d5eda9e8be7a3"
# 2. Setup cache directory
CACHE_DIR="$HOME/.cache/prisma-nix-fix/$PRISMA_VERSION"
mkdir -p "$CACHE_DIR"
# 3. Only download schema-engine (REQUIRED for migrations)
# Prisma 7 does not use the Rust query-engine by default, so we skip it to avoid 404s.
if [ ! -f "$CACHE_DIR/schema-engine" ]; then
echo ">> NixOS Fix: Downloading schema-engine for Prisma $PRISMA_VERSION..."
# Use -f to fail silently if the file is missing (though it shouldn't be for schema-engine)
curl -fL "https://binaries.prisma.sh/all_commits/$COMMIT/debian-openssl-3.0.x/schema-engine.gz" | gunzip > "$CACHE_DIR/schema-engine"
chmod +x "$CACHE_DIR/schema-engine"
fi
# 4. Export the specific variable for the Schema Engine
export PRISMA_SCHEMA_ENGINE_BINARY="$CACHE_DIR/schema-engine"
# 5. UNSET the query engine variables.
# Prisma 7 will fall back to its internal TypeScript engine (which works fine in FHS).
# If we leave these set to broken files (from the 404s), Prisma crashes.
unset PRISMA_QUERY_ENGINE_BINARY
unset PRISMA_QUERY_ENGINE_LIBRARY
unset PRISMA_FMT_BINARY
echo "------------------------------------------------------------"
echo " Prisma 7.x Shell (NixOS)"
echo " - Schema Engine: Pinned to Debian OpenSSL 3.0.x"
echo " - Query Engine: Using Prisma Default (TypeScript)"
echo "------------------------------------------------------------"
'';
}).env

212
src/index.ts Normal file
View file

@ -0,0 +1,212 @@
// src/index.ts
import { Hono } from 'hono'
import { PrismaClient } from '@prisma/client';
import { PrismaD1 } from '@prisma/adapter-d1';
import { Bindings, Variables, UserBody, LoginBody, PostCreationBody } from './types';
import { genSalt, hash, compare } from "bcrypt-ts";
import { sign_jwt } from './util/jwt'
import { authMiddleware } from './middleware/auth';
import { JsonObject } from '@prisma/client/runtime/client';
import { zValidator } from '@hono/zod-validator';
import { userCreationSchema, userLoginSchema, postCreationSchema } from './middleware/schemas';
import { cors } from 'hono/cors';
import { setCookie } from 'hono/cookie';
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
app.use('*', async (c, next) => {
const adapter = new PrismaD1(c.env.DB);
const prisma = new PrismaClient({ adapter });
c.set('prisma', prisma);
await next();
})
app.use('*', cors({
origin: 'http://localhost:5173',
allowHeaders: ['Content-Type', 'Authorization'], // Specify the headers your clients might send
allowMethods: ['POST', 'GET', 'OPTIONS', 'DELETE', 'PATCH', 'PUT'], // Specify the methods you use
maxAge: 600, // Optional: cache preflight response for 10 minutes
credentials: true
}));
app.get('/', (c) => {
return c.html("<h1>Hello!</h1>")
})
app.post('/users/create', zValidator('json', userCreationSchema), async (c) => {
const prisma = c.get('prisma');
const body = await c.req.json<UserBody>();
const salt = await genSalt(10);
const user = await prisma.user.create({
data: {
email: body.email,
name: body.name,
display_name: body.display_name,
password: await hash(body.password, salt),
},
})
const jwt = await sign_jwt({ id: user.id });
setCookie(c, 'auth_token', jwt, {
httpOnly: true,
secure: true,
sameSite: 'Lax',
path: '/',
maxAge: 60 * 60 * 24 * 7,
});
return c.json({ err: null, result: user })
})
app.post('/users/login', zValidator('json', userLoginSchema), async (c) => {
const prisma = c.get('prisma');
const body = await c.req.json<LoginBody>();
const user = await prisma.user.findUnique({
where: { email: body.email },
});
if (!user) return c.body("404 Not Found\n");
const result = await compare(body.password, user.password);
if (result) {
const jwt = await sign_jwt({ id: user.id });
setCookie(c, 'auth_token', jwt, {
httpOnly: true,
secure: true,
sameSite: 'Lax',
path: '/',
maxAge: 60 * 60 * 24 * 7,
});
return c.json({ err: null, result: user })
} else {
c.status(403);
return c.json({ err: 403 })
}
})
app.post('/posts/add', zValidator('json', postCreationSchema), authMiddleware, async (c) => {
const prisma = c.get('prisma');
const userId = c.get('userId');
const body = await c.req.json<PostCreationBody>();
const user = await prisma.user.findUnique({
where: { id: userId }
})
if (!user) {
return c.text("403 forbidden")
}
const post = await prisma.post.create({
data: {
title: body.title,
content: body.content,
author: { connect: { id: user.id } }
}
});
return c.json(post)
})
app.post('/posts/:id/reply', zValidator('json', postCreationSchema), authMiddleware, async (c) => {
const prisma = c.get('prisma');
const userId = c.get('userId');
const body = await c.req.json<PostCreationBody>();
// auth check
const user = await prisma.user.findUnique({
where: { id: userId }
})
if (!user) {
return c.text("403 forbidden")
}
const parentIdParameter = c.req.param("id");
if (isNaN(Number(parentIdParameter))) {
c.status(400)
return c.text("400 bad request")
}
const parentId = Number(parentIdParameter)
const parent_post = await prisma.post.findUnique({
where: {
id: parentId
}
});
if (!parent_post) {
return c.text("404 not found")
}
// actually make the post
const post = await prisma.post.create({
data: {
title: body.title,
content: body.content,
author: { connect: { id: user.id } },
parent: { connect: { id: parentId } }
}
});
return c.json(post)
})
// Helper to generate nested includes with limits
function getNestedReplies(depth: number, breadth: number): JsonObject {
if (depth === 0) return {};
return {
include: {
author: {
select: {
name: true,
display_name: true,
id: true
}
},
replies: {
take: breadth, // limit breadth
orderBy: { createdAt: 'desc' as const },
...getNestedReplies(depth - 1, breadth),
},
_count: {
select: { replies: true } // indicator of if there's more stuff to load
}
},
};
}
app.get('/posts/:id/view', async (c) => {
const prisma = c.get('prisma');
const post_id = c.req.param('id');
const post = await prisma.post.findMany({
where: {
id: Number(post_id),
},
...getNestedReplies(3, 5)
})
return c.json(post)
})
app.get('/feed', async (c) => {
const prisma = c.get('prisma');
const posts = await prisma.post.findMany({
where: {
parentId: null,
},
orderBy: {
createdAt: 'desc'
},
take: 10,
...getNestedReplies(2, 3)
});
return c.json(posts);
})
export default app

24
src/middleware/auth.ts Normal file
View file

@ -0,0 +1,24 @@
// src/middleware/auth.ts
import { createMiddleware } from 'hono/factory'
import { verify_jwt } from '../util/jwt'
import { Bindings, Variables } from '../types'
import { getCookie } from 'hono/cookie';
export const authMiddleware = createMiddleware<{
Bindings: Bindings,
Variables: Variables
}>(async (c, next) => {
const auth = getCookie(c, 'auth_token');
if (!auth) {
return c.json({ err: 'Unauthorized' }, 401)
}
try {
const payload = await verify_jwt(auth)
c.set('userId', payload.id)
await next()
} catch {
return c.text("Invalid Token", 401)
}
})

16
src/middleware/prisma.ts Normal file
View file

@ -0,0 +1,16 @@
import { createMiddleware } from 'hono/factory'
import { PrismaClient } from '@prisma/client'
import { PrismaD1 } from '@prisma/adapter-d1'
import { Bindings, Variables } from '../types'
export const prismaMiddleware = createMiddleware<{
Bindings: Bindings,
Variables: Variables
}>(async (c, next) => {
const adapter = new PrismaD1(c.env.DB)
const prisma = new PrismaClient({ adapter })
c.set('prisma', prisma)
await next()
})

21
src/middleware/schemas.ts Normal file
View file

@ -0,0 +1,21 @@
// src/middleware/schemas.ts
import * as z from 'zod';
export const userCreationSchema = z.object({
email: z.email().max(255),
name: z.string().min(3).max(32),
display_name: z.string().min(3).max(32),
password: z.string().min(8).max(255),
});
export const userLoginSchema = z.object({
email: z.email().max(255),
password: z.string().min(8).max(255)
});
export const postCreationSchema = z.object({
title: z.string().min(1).max(128),
content: z.string().min(1).max(2147483647)
})

30
src/types.ts Normal file
View file

@ -0,0 +1,30 @@
// src/types.ts
import { PrismaClient } from '@prisma/client';
export interface Bindings {
// @ts-expect-error Defined by God Himself. (idk ask Cloudflare)
DB: D1Database;
}
export interface Variables {
prisma: PrismaClient;
userId: number;
postId: number;
}
export interface UserBody {
email: string
name: string
display_name: string
password: string
}
export interface LoginBody {
email: string
password: string
}
export interface PostCreationBody {
title: string,
content: string,
}

28
src/util/jwt.ts Normal file
View file

@ -0,0 +1,28 @@
import * as jose from 'jose'
const issuer = "Dingaling Industries LLC";
// @ts-expect-error Defined by CF.
const secret = new TextEncoder().encode("heilstalin");
interface PayloadBody {
id: number
}
export async function sign_jwt(payload: jose.JWTPayload) {
const alg = 'HS256';
const jwt = await new jose.SignJWT(payload)
.setIssuedAt()
.setIssuer(issuer)
.setExpirationTime('2w')
.setProtectedHeader({ alg })
.sign(secret)
return jwt;
}
export async function verify_jwt(jwt: string) {
const { payload } = await jose.jwtVerify(jwt, secret);
return payload as unknown as PayloadBody;
}

14
tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"lib": [
"ESNext"
],
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
},
}

16
wrangler.jsonc Normal file
View file

@ -0,0 +1,16 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "anum-backend",
"compatibility_date": "2025-08-03",
"compatibility_flags": [
"nodejs_compat"
],
"main": "./src/index.ts",
"d1_databases": [
{
"binding": "DB",
"database_name": "anum",
"database_id": "to be filled when i get remote cloudflare workers up"
}
]
}