diff options
Diffstat (limited to 'pkgs/server')
25 files changed, 0 insertions, 2483 deletions
diff --git a/pkgs/server/drizzle.config.ts b/pkgs/server/drizzle.config.ts deleted file mode 100644 index 0e4596b..0000000 --- a/pkgs/server/drizzle.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from "drizzle-kit"; - -const databaseUrl = process.env.DATABASE_URL; -if (!databaseUrl) { - throw new Error("DATABASE_URL environment variable is not set"); -} - -export default defineConfig({ - out: "./drizzle", - schema: "./src/db/schema.ts", - dialect: "postgresql", - dbCredentials: { - url: databaseUrl, - }, -}); diff --git a/pkgs/server/drizzle/0000_cynical_zeigeist.sql b/pkgs/server/drizzle/0000_cynical_zeigeist.sql deleted file mode 100644 index 3034102..0000000 --- a/pkgs/server/drizzle/0000_cynical_zeigeist.sql +++ /dev/null @@ -1,58 +0,0 @@ -CREATE TABLE "cards" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "deck_id" uuid NOT NULL, - "front" text NOT NULL, - "back" text NOT NULL, - "state" smallint DEFAULT 0 NOT NULL, - "due" timestamp with time zone DEFAULT now() NOT NULL, - "stability" real DEFAULT 0 NOT NULL, - "difficulty" real DEFAULT 0 NOT NULL, - "elapsed_days" integer DEFAULT 0 NOT NULL, - "scheduled_days" integer DEFAULT 0 NOT NULL, - "reps" integer DEFAULT 0 NOT NULL, - "lapses" integer DEFAULT 0 NOT NULL, - "last_review" timestamp with time zone, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - "deleted_at" timestamp with time zone, - "sync_version" integer DEFAULT 0 NOT NULL -); ---> statement-breakpoint -CREATE TABLE "decks" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "user_id" uuid NOT NULL, - "name" varchar(255) NOT NULL, - "description" text, - "new_cards_per_day" integer DEFAULT 20 NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - "deleted_at" timestamp with time zone, - "sync_version" integer DEFAULT 0 NOT NULL -); ---> statement-breakpoint -CREATE TABLE "review_logs" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "card_id" uuid NOT NULL, - "user_id" uuid NOT NULL, - "rating" smallint NOT NULL, - "state" smallint NOT NULL, - "scheduled_days" integer NOT NULL, - "elapsed_days" integer NOT NULL, - "reviewed_at" timestamp with time zone DEFAULT now() NOT NULL, - "duration_ms" integer, - "sync_version" integer DEFAULT 0 NOT NULL -); ---> statement-breakpoint -CREATE TABLE "users" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "username" varchar(255) NOT NULL, - "password_hash" varchar(255) NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "users_username_unique" UNIQUE("username") -); ---> statement-breakpoint -ALTER TABLE "cards" ADD CONSTRAINT "cards_deck_id_decks_id_fk" FOREIGN KEY ("deck_id") REFERENCES "public"."decks"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "decks" ADD CONSTRAINT "decks_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "review_logs" ADD CONSTRAINT "review_logs_card_id_cards_id_fk" FOREIGN KEY ("card_id") REFERENCES "public"."cards"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "review_logs" ADD CONSTRAINT "review_logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
\ No newline at end of file diff --git a/pkgs/server/drizzle/0001_spotty_jane_foster.sql b/pkgs/server/drizzle/0001_spotty_jane_foster.sql deleted file mode 100644 index 417408f..0000000 --- a/pkgs/server/drizzle/0001_spotty_jane_foster.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE "refresh_tokens" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "user_id" uuid NOT NULL, - "token_hash" varchar(255) NOT NULL, - "expires_at" timestamp with time zone NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL -); ---> statement-breakpoint -ALTER TABLE "refresh_tokens" ADD CONSTRAINT "refresh_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
\ No newline at end of file diff --git a/pkgs/server/drizzle/meta/0000_snapshot.json b/pkgs/server/drizzle/meta/0000_snapshot.json deleted file mode 100644 index 16a3f68..0000000 --- a/pkgs/server/drizzle/meta/0000_snapshot.json +++ /dev/null @@ -1,403 +0,0 @@ -{ - "id": "d2f779a2-d302-4fe3-91bb-a541025dbe4a", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.cards": { - "name": "cards", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "deck_id": { - "name": "deck_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "front": { - "name": "front", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "back": { - "name": "back", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "state": { - "name": "state", - "type": "smallint", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "due": { - "name": "due", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "stability": { - "name": "stability", - "type": "real", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "difficulty": { - "name": "difficulty", - "type": "real", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "elapsed_days": { - "name": "elapsed_days", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "scheduled_days": { - "name": "scheduled_days", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "reps": { - "name": "reps", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "lapses": { - "name": "lapses", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "last_review": { - "name": "last_review", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "sync_version": { - "name": "sync_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "cards_deck_id_decks_id_fk": { - "name": "cards_deck_id_decks_id_fk", - "tableFrom": "cards", - "tableTo": "decks", - "columnsFrom": [ - "deck_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.decks": { - "name": "decks", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "new_cards_per_day": { - "name": "new_cards_per_day", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 20 - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "sync_version": { - "name": "sync_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "decks_user_id_users_id_fk": { - "name": "decks_user_id_users_id_fk", - "tableFrom": "decks", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.review_logs": { - "name": "review_logs", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "card_id": { - "name": "card_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "rating": { - "name": "rating", - "type": "smallint", - "primaryKey": false, - "notNull": true - }, - "state": { - "name": "state", - "type": "smallint", - "primaryKey": false, - "notNull": true - }, - "scheduled_days": { - "name": "scheduled_days", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "elapsed_days": { - "name": "elapsed_days", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "reviewed_at": { - "name": "reviewed_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "duration_ms": { - "name": "duration_ms", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "sync_version": { - "name": "sync_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "review_logs_card_id_cards_id_fk": { - "name": "review_logs_card_id_cards_id_fk", - "tableFrom": "review_logs", - "tableTo": "cards", - "columnsFrom": [ - "card_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "review_logs_user_id_users_id_fk": { - "name": "review_logs_user_id_users_id_fk", - "tableFrom": "review_logs", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "username": { - "name": "username", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "password_hash": { - "name": "password_hash", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_username_unique": { - "name": "users_username_unique", - "nullsNotDistinct": false, - "columns": [ - "username" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -}
\ No newline at end of file diff --git a/pkgs/server/drizzle/meta/0001_snapshot.json b/pkgs/server/drizzle/meta/0001_snapshot.json deleted file mode 100644 index 55c3999..0000000 --- a/pkgs/server/drizzle/meta/0001_snapshot.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "id": "d3d0fb16-7e44-4217-916e-a5edc9ab7a16", - "prevId": "d2f779a2-d302-4fe3-91bb-a541025dbe4a", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.cards": { - "name": "cards", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "deck_id": { - "name": "deck_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "front": { - "name": "front", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "back": { - "name": "back", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "state": { - "name": "state", - "type": "smallint", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "due": { - "name": "due", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "stability": { - "name": "stability", - "type": "real", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "difficulty": { - "name": "difficulty", - "type": "real", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "elapsed_days": { - "name": "elapsed_days", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "scheduled_days": { - "name": "scheduled_days", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "reps": { - "name": "reps", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "lapses": { - "name": "lapses", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "last_review": { - "name": "last_review", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "sync_version": { - "name": "sync_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "cards_deck_id_decks_id_fk": { - "name": "cards_deck_id_decks_id_fk", - "tableFrom": "cards", - "tableTo": "decks", - "columnsFrom": [ - "deck_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.decks": { - "name": "decks", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "new_cards_per_day": { - "name": "new_cards_per_day", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 20 - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "sync_version": { - "name": "sync_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "decks_user_id_users_id_fk": { - "name": "decks_user_id_users_id_fk", - "tableFrom": "decks", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.refresh_tokens": { - "name": "refresh_tokens", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "token_hash": { - "name": "token_hash", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "refresh_tokens_user_id_users_id_fk": { - "name": "refresh_tokens_user_id_users_id_fk", - "tableFrom": "refresh_tokens", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.review_logs": { - "name": "review_logs", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "card_id": { - "name": "card_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "rating": { - "name": "rating", - "type": "smallint", - "primaryKey": false, - "notNull": true - }, - "state": { - "name": "state", - "type": "smallint", - "primaryKey": false, - "notNull": true - }, - "scheduled_days": { - "name": "scheduled_days", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "elapsed_days": { - "name": "elapsed_days", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "reviewed_at": { - "name": "reviewed_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "duration_ms": { - "name": "duration_ms", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "sync_version": { - "name": "sync_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "review_logs_card_id_cards_id_fk": { - "name": "review_logs_card_id_cards_id_fk", - "tableFrom": "review_logs", - "tableTo": "cards", - "columnsFrom": [ - "card_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "review_logs_user_id_users_id_fk": { - "name": "review_logs_user_id_users_id_fk", - "tableFrom": "review_logs", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "username": { - "name": "username", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "password_hash": { - "name": "password_hash", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_username_unique": { - "name": "users_username_unique", - "nullsNotDistinct": false, - "columns": [ - "username" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -}
\ No newline at end of file diff --git a/pkgs/server/drizzle/meta/_journal.json b/pkgs/server/drizzle/meta/_journal.json deleted file mode 100644 index f245fa6..0000000 --- a/pkgs/server/drizzle/meta/_journal.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1764706245996, - "tag": "0000_cynical_zeigeist", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1764708169736, - "tag": "0001_spotty_jane_foster", - "breakpoints": true - } - ] -}
\ No newline at end of file diff --git a/pkgs/server/package.json b/pkgs/server/package.json deleted file mode 100644 index 4159084..0000000 --- a/pkgs/server/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@kioku/server", - "version": "0.1.0", - "private": true, - "main": "dist/index.js", - "scripts": { - "dev": "node --watch src/index.ts", - "build": "tsc", - "start": "node dist/index.js", - "test": "vitest run", - "test:watch": "vitest", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate", - "db:push": "drizzle-kit push", - "db:studio": "drizzle-kit studio" - }, - "author": "nsfisis", - "license": "MIT", - "packageManager": "pnpm@10.23.0", - "type": "module", - "dependencies": { - "@hono/node-server": "^1.19.6", - "@kioku/shared": "workspace:*", - "argon2": "^0.44.0", - "drizzle-orm": "^0.44.7", - "hono": "^4.10.7", - "pg": "^8.16.3" - }, - "devDependencies": { - "@types/node": "^24.10.1", - "@types/pg": "^8.15.6", - "drizzle-kit": "^0.31.7", - "vitest": "^4.0.14" - } -} diff --git a/pkgs/server/src/db/index.ts b/pkgs/server/src/db/index.ts deleted file mode 100644 index 6730947..0000000 --- a/pkgs/server/src/db/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { drizzle } from "drizzle-orm/node-postgres"; -import * as schema from "./schema"; - -const databaseUrl = process.env.DATABASE_URL; - -if (!databaseUrl) { - throw new Error("DATABASE_URL environment variable is not set"); -} - -export const db = drizzle(databaseUrl, { schema }); - -export * from "./schema"; diff --git a/pkgs/server/src/db/schema.ts b/pkgs/server/src/db/schema.ts deleted file mode 100644 index 4b9631f..0000000 --- a/pkgs/server/src/db/schema.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { - integer, - pgTable, - real, - smallint, - text, - timestamp, - uuid, - varchar, -} from "drizzle-orm/pg-core"; - -// Card states for FSRS algorithm -export const CardState = { - New: 0, - Learning: 1, - Review: 2, - Relearning: 3, -} as const; - -// Rating values for reviews -export const Rating = { - Again: 1, - Hard: 2, - Good: 3, - Easy: 4, -} as const; - -export const users = pgTable("users", { - id: uuid("id").primaryKey().defaultRandom(), - username: varchar("username", { length: 255 }).notNull().unique(), - passwordHash: varchar("password_hash", { length: 255 }).notNull(), - createdAt: timestamp("created_at", { withTimezone: true }) - .notNull() - .defaultNow(), - updatedAt: timestamp("updated_at", { withTimezone: true }) - .notNull() - .defaultNow(), -}); - -export const refreshTokens = pgTable("refresh_tokens", { - id: uuid("id").primaryKey().defaultRandom(), - userId: uuid("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - tokenHash: varchar("token_hash", { length: 255 }).notNull(), - expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), - createdAt: timestamp("created_at", { withTimezone: true }) - .notNull() - .defaultNow(), -}); - -export const decks = pgTable("decks", { - id: uuid("id").primaryKey().defaultRandom(), - userId: uuid("user_id") - .notNull() - .references(() => users.id), - name: varchar("name", { length: 255 }).notNull(), - description: text("description"), - newCardsPerDay: integer("new_cards_per_day").notNull().default(20), - createdAt: timestamp("created_at", { withTimezone: true }) - .notNull() - .defaultNow(), - updatedAt: timestamp("updated_at", { withTimezone: true }) - .notNull() - .defaultNow(), - deletedAt: timestamp("deleted_at", { withTimezone: true }), - syncVersion: integer("sync_version").notNull().default(0), -}); - -export const cards = pgTable("cards", { - id: uuid("id").primaryKey().defaultRandom(), - deckId: uuid("deck_id") - .notNull() - .references(() => decks.id), - front: text("front").notNull(), - back: text("back").notNull(), - - // FSRS fields - state: smallint("state").notNull().default(CardState.New), - due: timestamp("due", { withTimezone: true }).notNull().defaultNow(), - stability: real("stability").notNull().default(0), - difficulty: real("difficulty").notNull().default(0), - elapsedDays: integer("elapsed_days").notNull().default(0), - scheduledDays: integer("scheduled_days").notNull().default(0), - reps: integer("reps").notNull().default(0), - lapses: integer("lapses").notNull().default(0), - lastReview: timestamp("last_review", { withTimezone: true }), - - createdAt: timestamp("created_at", { withTimezone: true }) - .notNull() - .defaultNow(), - updatedAt: timestamp("updated_at", { withTimezone: true }) - .notNull() - .defaultNow(), - deletedAt: timestamp("deleted_at", { withTimezone: true }), - syncVersion: integer("sync_version").notNull().default(0), -}); - -export const reviewLogs = pgTable("review_logs", { - id: uuid("id").primaryKey().defaultRandom(), - cardId: uuid("card_id") - .notNull() - .references(() => cards.id), - userId: uuid("user_id") - .notNull() - .references(() => users.id), - rating: smallint("rating").notNull(), - state: smallint("state").notNull(), - scheduledDays: integer("scheduled_days").notNull(), - elapsedDays: integer("elapsed_days").notNull(), - reviewedAt: timestamp("reviewed_at", { withTimezone: true }) - .notNull() - .defaultNow(), - durationMs: integer("duration_ms"), - syncVersion: integer("sync_version").notNull().default(0), -}); diff --git a/pkgs/server/src/index.test.ts b/pkgs/server/src/index.test.ts deleted file mode 100644 index 216e965..0000000 --- a/pkgs/server/src/index.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { app } from "./index"; - -describe("Hono app", () => { - describe("GET /api/health", () => { - it("returns ok status", async () => { - const res = await app.request("/api/health"); - expect(res.status).toBe(200); - expect(await res.json()).toEqual({ status: "ok" }); - }); - }); -}); diff --git a/pkgs/server/src/index.ts b/pkgs/server/src/index.ts deleted file mode 100644 index a0ae0a4..0000000 --- a/pkgs/server/src/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { serve } from "@hono/node-server"; -import { Hono } from "hono"; -import { logger } from "hono/logger"; -import { errorHandler } from "./middleware"; -import { auth } from "./routes"; - -const app = new Hono(); - -app.use("*", logger()); -app.onError(errorHandler); - -app.get("/", (c) => { - return c.json({ message: "Kioku API" }); -}); - -app.get("/api/health", (c) => { - return c.json({ status: "ok" }); -}); - -app.route("/api/auth", auth); - -const port = Number(process.env.PORT) || 3000; -console.log(`Server is running on port ${port}`); - -serve({ - fetch: app.fetch, - port, -}); - -export { app }; diff --git a/pkgs/server/src/middleware/auth.test.ts b/pkgs/server/src/middleware/auth.test.ts deleted file mode 100644 index 8c4286b..0000000 --- a/pkgs/server/src/middleware/auth.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { Hono } from "hono"; -import { sign } from "hono/jwt"; -import { beforeEach, describe, expect, it } from "vitest"; -import { authMiddleware, getAuthUser } from "./auth"; -import { errorHandler } from "./error-handler"; - -const JWT_SECRET = process.env.JWT_SECRET || "test-secret"; - -interface ErrorResponse { - error: { - code: string; - message: string; - }; -} - -interface SuccessResponse { - userId: string; -} - -describe("authMiddleware", () => { - let app: Hono; - - beforeEach(() => { - app = new Hono(); - app.onError(errorHandler); - app.use("/protected/*", authMiddleware); - app.get("/protected/resource", (c) => { - const user = getAuthUser(c); - return c.json({ userId: user.id }); - }); - }); - - it("allows access with valid token", async () => { - const now = Math.floor(Date.now() / 1000); - const token = await sign( - { - sub: "user-123", - iat: now, - exp: now + 3600, - }, - JWT_SECRET, - ); - - const res = await app.request("/protected/resource", { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - expect(res.status).toBe(200); - const body = (await res.json()) as SuccessResponse; - expect(body.userId).toBe("user-123"); - }); - - it("returns 401 when Authorization header is missing", async () => { - const res = await app.request("/protected/resource"); - - expect(res.status).toBe(401); - const body = (await res.json()) as ErrorResponse; - expect(body.error.code).toBe("MISSING_AUTH"); - }); - - it("returns 401 for invalid Authorization format", async () => { - const res = await app.request("/protected/resource", { - headers: { - Authorization: "Basic sometoken", - }, - }); - - expect(res.status).toBe(401); - const body = (await res.json()) as ErrorResponse; - expect(body.error.code).toBe("INVALID_AUTH_FORMAT"); - }); - - it("returns 401 for empty Bearer token", async () => { - const res = await app.request("/protected/resource", { - headers: { - Authorization: "Bearer", - }, - }); - - expect(res.status).toBe(401); - const body = (await res.json()) as ErrorResponse; - expect(body.error.code).toBe("INVALID_AUTH_FORMAT"); - }); - - it("returns 401 for expired token", async () => { - const now = Math.floor(Date.now() / 1000); - const token = await sign( - { - sub: "user-123", - iat: now - 7200, - exp: now - 3600, // expired 1 hour ago - }, - JWT_SECRET, - ); - - const res = await app.request("/protected/resource", { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - expect(res.status).toBe(401); - const body = (await res.json()) as ErrorResponse; - expect(body.error.code).toBe("INVALID_TOKEN"); - }); - - it("returns 401 for invalid token", async () => { - const res = await app.request("/protected/resource", { - headers: { - Authorization: "Bearer invalid.token.here", - }, - }); - - expect(res.status).toBe(401); - const body = (await res.json()) as ErrorResponse; - expect(body.error.code).toBe("INVALID_TOKEN"); - }); - - it("returns 401 for token signed with wrong secret", async () => { - const now = Math.floor(Date.now() / 1000); - const token = await sign( - { - sub: "user-123", - iat: now, - exp: now + 3600, - }, - "wrong-secret", - ); - - const res = await app.request("/protected/resource", { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - expect(res.status).toBe(401); - const body = (await res.json()) as ErrorResponse; - expect(body.error.code).toBe("INVALID_TOKEN"); - }); -}); - -describe("getAuthUser", () => { - it("returns user from context when authenticated", async () => { - const app = new Hono(); - app.onError(errorHandler); - app.use("/test", authMiddleware); - app.get("/test", (c) => { - const user = getAuthUser(c); - return c.json({ id: user.id }); - }); - - const now = Math.floor(Date.now() / 1000); - const token = await sign( - { - sub: "test-user-456", - iat: now, - exp: now + 3600, - }, - JWT_SECRET, - ); - - const res = await app.request("/test", { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - expect(res.status).toBe(200); - const body = (await res.json()) as { id: string }; - expect(body.id).toBe("test-user-456"); - }); - - it("throws when user is not in context", async () => { - const app = new Hono(); - app.onError(errorHandler); - // Note: no authMiddleware applied - app.get("/unprotected", (c) => { - const user = getAuthUser(c); - return c.json({ id: user.id }); - }); - - const res = await app.request("/unprotected"); - - expect(res.status).toBe(401); - const body = (await res.json()) as ErrorResponse; - expect(body.error.code).toBe("NOT_AUTHENTICATED"); - }); -}); diff --git a/pkgs/server/src/middleware/auth.ts b/pkgs/server/src/middleware/auth.ts deleted file mode 100644 index c295834..0000000 --- a/pkgs/server/src/middleware/auth.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { Context, Next } from "hono"; -import { verify } from "hono/jwt"; -import { Errors } from "./error-handler"; - -const JWT_SECRET = process.env.JWT_SECRET; -if (!JWT_SECRET) { - throw new Error("JWT_SECRET environment variable is required"); -} - -export interface AuthUser { - id: string; -} - -interface JWTPayload { - sub: string; - iat: number; - exp: number; -} - -/** - * Auth middleware that validates JWT tokens from Authorization header - * Sets the authenticated user in context variables - */ -export async function authMiddleware(c: Context, next: Next) { - const authHeader = c.req.header("Authorization"); - - if (!authHeader) { - throw Errors.unauthorized("Missing Authorization header", "MISSING_AUTH"); - } - - if (!authHeader.startsWith("Bearer ")) { - throw Errors.unauthorized( - "Invalid Authorization header format", - "INVALID_AUTH_FORMAT", - ); - } - - const token = authHeader.slice(7); - - try { - const payload = (await verify(token, JWT_SECRET)) as unknown as JWTPayload; - - const user: AuthUser = { - id: payload.sub, - }; - - c.set("user", user); - - await next(); - } catch { - throw Errors.unauthorized("Invalid or expired token", "INVALID_TOKEN"); - } -} - -/** - * Helper function to get the authenticated user from context - * Throws if user is not authenticated - */ -export function getAuthUser(c: Context): AuthUser { - const user = c.get("user") as AuthUser | undefined; - if (!user) { - throw Errors.unauthorized("Not authenticated", "NOT_AUTHENTICATED"); - } - return user; -} diff --git a/pkgs/server/src/middleware/error-handler.test.ts b/pkgs/server/src/middleware/error-handler.test.ts deleted file mode 100644 index 21d6fc1..0000000 --- a/pkgs/server/src/middleware/error-handler.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { Hono } from "hono"; -import { describe, expect, it } from "vitest"; -import { AppError, Errors, errorHandler } from "./error-handler"; - -function createTestApp() { - const app = new Hono(); - app.onError(errorHandler); - return app; -} - -describe("errorHandler", () => { - describe("AppError handling", () => { - it("returns correct status and message for AppError", async () => { - const app = createTestApp(); - app.get("/test", () => { - throw new AppError("Custom error", 400, "CUSTOM_ERROR"); - }); - - const res = await app.request("/test"); - expect(res.status).toBe(400); - expect(await res.json()).toEqual({ - error: { - message: "Custom error", - code: "CUSTOM_ERROR", - }, - }); - }); - - it("uses default values for AppError", async () => { - const app = createTestApp(); - app.get("/test", () => { - throw new AppError("Something went wrong"); - }); - - const res = await app.request("/test"); - expect(res.status).toBe(500); - expect(await res.json()).toEqual({ - error: { - message: "Something went wrong", - code: "INTERNAL_ERROR", - }, - }); - }); - }); - - describe("Errors factory functions", () => { - it("handles badRequest error", async () => { - const app = createTestApp(); - app.get("/test", () => { - throw Errors.badRequest("Invalid input"); - }); - - const res = await app.request("/test"); - expect(res.status).toBe(400); - expect(await res.json()).toEqual({ - error: { - message: "Invalid input", - code: "BAD_REQUEST", - }, - }); - }); - - it("handles unauthorized error", async () => { - const app = createTestApp(); - app.get("/test", () => { - throw Errors.unauthorized(); - }); - - const res = await app.request("/test"); - expect(res.status).toBe(401); - expect(await res.json()).toEqual({ - error: { - message: "Unauthorized", - code: "UNAUTHORIZED", - }, - }); - }); - - it("handles forbidden error", async () => { - const app = createTestApp(); - app.get("/test", () => { - throw Errors.forbidden("Access denied"); - }); - - const res = await app.request("/test"); - expect(res.status).toBe(403); - expect(await res.json()).toEqual({ - error: { - message: "Access denied", - code: "FORBIDDEN", - }, - }); - }); - - it("handles notFound error", async () => { - const app = createTestApp(); - app.get("/test", () => { - throw Errors.notFound("Resource not found"); - }); - - const res = await app.request("/test"); - expect(res.status).toBe(404); - expect(await res.json()).toEqual({ - error: { - message: "Resource not found", - code: "NOT_FOUND", - }, - }); - }); - - it("handles conflict error", async () => { - const app = createTestApp(); - app.get("/test", () => { - throw Errors.conflict("Already exists"); - }); - - const res = await app.request("/test"); - expect(res.status).toBe(409); - expect(await res.json()).toEqual({ - error: { - message: "Already exists", - code: "CONFLICT", - }, - }); - }); - - it("handles validationError", async () => { - const app = createTestApp(); - app.get("/test", () => { - throw Errors.validationError("Invalid data"); - }); - - const res = await app.request("/test"); - expect(res.status).toBe(422); - expect(await res.json()).toEqual({ - error: { - message: "Invalid data", - code: "VALIDATION_ERROR", - }, - }); - }); - - it("handles internal error", async () => { - const app = createTestApp(); - app.get("/test", () => { - throw Errors.internal("Database connection failed"); - }); - - const res = await app.request("/test"); - expect(res.status).toBe(500); - expect(await res.json()).toEqual({ - error: { - message: "Database connection failed", - code: "INTERNAL_ERROR", - }, - }); - }); - }); - - describe("unknown error handling", () => { - it("handles generic Error with 500 status", async () => { - const app = createTestApp(); - app.get("/test", () => { - throw new Error("Unexpected error"); - }); - - const res = await app.request("/test"); - expect(res.status).toBe(500); - const body = (await res.json()) as { error: { code: string } }; - expect(body.error.code).toBe("INTERNAL_ERROR"); - }); - }); -}); diff --git a/pkgs/server/src/middleware/error-handler.ts b/pkgs/server/src/middleware/error-handler.ts deleted file mode 100644 index 7b92940..0000000 --- a/pkgs/server/src/middleware/error-handler.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { Context } from "hono"; -import { HTTPException } from "hono/http-exception"; -import type { ContentfulStatusCode } from "hono/utils/http-status"; - -/** - * Application-specific error with status code and optional details - */ -export class AppError extends Error { - readonly statusCode: ContentfulStatusCode; - readonly code: string; - - constructor( - message: string, - statusCode: ContentfulStatusCode = 500, - code = "INTERNAL_ERROR", - ) { - super(message); - this.name = "AppError"; - this.statusCode = statusCode; - this.code = code; - } -} - -/** - * Common error factory functions - */ -export const Errors = { - badRequest: (message = "Bad request", code = "BAD_REQUEST") => - new AppError(message, 400, code), - - unauthorized: (message = "Unauthorized", code = "UNAUTHORIZED") => - new AppError(message, 401, code), - - forbidden: (message = "Forbidden", code = "FORBIDDEN") => - new AppError(message, 403, code), - - notFound: (message = "Not found", code = "NOT_FOUND") => - new AppError(message, 404, code), - - conflict: (message = "Conflict", code = "CONFLICT") => - new AppError(message, 409, code), - - validationError: (message = "Validation failed", code = "VALIDATION_ERROR") => - new AppError(message, 422, code), - - internal: (message = "Internal server error", code = "INTERNAL_ERROR") => - new AppError(message, 500, code), -}; - -interface ErrorResponse { - error: { - message: string; - code: string; - }; -} - -/** - * Global error handler middleware for Hono - */ -export function errorHandler(err: Error, c: Context): Response { - // Handle AppError - if (err instanceof AppError) { - const response: ErrorResponse = { - error: { - message: err.message, - code: err.code, - }, - }; - return c.json(response, err.statusCode); - } - - // Handle Hono's HTTPException - if (err instanceof HTTPException) { - const response: ErrorResponse = { - error: { - message: err.message, - code: "HTTP_ERROR", - }, - }; - return c.json(response, err.status as ContentfulStatusCode); - } - - // Handle unknown errors - console.error("Unhandled error:", err); - const response: ErrorResponse = { - error: { - message: - process.env.NODE_ENV === "production" - ? "Internal server error" - : err.message, - code: "INTERNAL_ERROR", - }, - }; - return c.json(response, 500); -} diff --git a/pkgs/server/src/middleware/index.ts b/pkgs/server/src/middleware/index.ts deleted file mode 100644 index 57de4dd..0000000 --- a/pkgs/server/src/middleware/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { type AuthUser, authMiddleware, getAuthUser } from "./auth"; -export { AppError, Errors, errorHandler } from "./error-handler"; diff --git a/pkgs/server/src/repositories/index.ts b/pkgs/server/src/repositories/index.ts deleted file mode 100644 index f1bcfb1..0000000 --- a/pkgs/server/src/repositories/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { refreshTokenRepository } from "./refresh-token"; -export * from "./types"; -export { userRepository } from "./user"; diff --git a/pkgs/server/src/repositories/refresh-token.ts b/pkgs/server/src/repositories/refresh-token.ts deleted file mode 100644 index 82302df..0000000 --- a/pkgs/server/src/repositories/refresh-token.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { and, eq, gt } from "drizzle-orm"; -import { db, refreshTokens } from "../db"; -import type { RefreshTokenRepository } from "./types"; - -export const refreshTokenRepository: RefreshTokenRepository = { - async findValidToken(tokenHash) { - const [token] = await db - .select({ - id: refreshTokens.id, - userId: refreshTokens.userId, - expiresAt: refreshTokens.expiresAt, - }) - .from(refreshTokens) - .where( - and( - eq(refreshTokens.tokenHash, tokenHash), - gt(refreshTokens.expiresAt, new Date()), - ), - ) - .limit(1); - return token; - }, - - async create(data) { - await db.insert(refreshTokens).values({ - userId: data.userId, - tokenHash: data.tokenHash, - expiresAt: data.expiresAt, - }); - }, - - async deleteById(id) { - await db.delete(refreshTokens).where(eq(refreshTokens.id, id)); - }, -}; diff --git a/pkgs/server/src/repositories/types.ts b/pkgs/server/src/repositories/types.ts deleted file mode 100644 index 1ab4bdc..0000000 --- a/pkgs/server/src/repositories/types.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Repository types for abstracting database operations - */ - -export interface User { - id: string; - username: string; - passwordHash: string; - createdAt: Date; - updatedAt: Date; -} - -export interface UserPublic { - id: string; - username: string; - createdAt: Date; -} - -export interface RefreshToken { - id: string; - userId: string; - tokenHash: string; - expiresAt: Date; - createdAt: Date; -} - -export interface UserRepository { - findByUsername( - username: string, - ): Promise<Pick<User, "id" | "username" | "passwordHash"> | undefined>; - existsByUsername(username: string): Promise<boolean>; - create(data: { username: string; passwordHash: string }): Promise<UserPublic>; - findById(id: string): Promise<Pick<User, "id" | "username"> | undefined>; -} - -export interface RefreshTokenRepository { - findValidToken( - tokenHash: string, - ): Promise<Pick<RefreshToken, "id" | "userId" | "expiresAt"> | undefined>; - create(data: { - userId: string; - tokenHash: string; - expiresAt: Date; - }): Promise<void>; - deleteById(id: string): Promise<void>; -} diff --git a/pkgs/server/src/repositories/user.ts b/pkgs/server/src/repositories/user.ts deleted file mode 100644 index 7917632..0000000 --- a/pkgs/server/src/repositories/user.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { eq } from "drizzle-orm"; -import { db, users } from "../db"; -import type { UserPublic, UserRepository } from "./types"; - -export const userRepository: UserRepository = { - async findByUsername(username) { - const [user] = await db - .select({ - id: users.id, - username: users.username, - passwordHash: users.passwordHash, - }) - .from(users) - .where(eq(users.username, username)) - .limit(1); - return user; - }, - - async existsByUsername(username) { - const [user] = await db - .select({ id: users.id }) - .from(users) - .where(eq(users.username, username)) - .limit(1); - return user !== undefined; - }, - - async create(data): Promise<UserPublic> { - const [newUser] = await db - .insert(users) - .values({ - username: data.username, - passwordHash: data.passwordHash, - }) - .returning({ - id: users.id, - username: users.username, - createdAt: users.createdAt, - }); - // Insert with returning should always return the created row - return newUser!; - }, - - async findById(id) { - const [user] = await db - .select({ - id: users.id, - username: users.username, - }) - .from(users) - .where(eq(users.id, id)) - .limit(1); - return user; - }, -}; diff --git a/pkgs/server/src/routes/auth.test.ts b/pkgs/server/src/routes/auth.test.ts deleted file mode 100644 index 34eb2b6..0000000 --- a/pkgs/server/src/routes/auth.test.ts +++ /dev/null @@ -1,428 +0,0 @@ -import { Hono } from "hono"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { errorHandler } from "../middleware"; -import type { - RefreshTokenRepository, - UserPublic, - UserRepository, -} from "../repositories"; -import { createAuthRouter } from "./auth"; - -vi.mock("argon2", () => ({ - hash: vi.fn((password: string) => Promise.resolve(`hashed_${password}`)), - verify: vi.fn((hash: string, password: string) => - Promise.resolve(hash === `hashed_${password}`), - ), -})); - -function createMockUserRepo(): UserRepository { - return { - findByUsername: vi.fn(), - existsByUsername: vi.fn(), - create: vi.fn(), - findById: vi.fn(), - }; -} - -function createMockRefreshTokenRepo(): RefreshTokenRepository { - return { - findValidToken: vi.fn(), - create: vi.fn(), - deleteById: vi.fn(), - }; -} - -interface RegisterResponse { - user?: { - id: string; - username: string; - createdAt: string; - }; - error?: { - code: string; - message: string; - }; -} - -interface LoginResponse { - accessToken?: string; - refreshToken?: string; - user?: { - id: string; - username: string; - }; - error?: { - code: string; - message: string; - }; -} - -describe("POST /register", () => { - let app: Hono; - let mockUserRepo: ReturnType<typeof createMockUserRepo>; - let mockRefreshTokenRepo: ReturnType<typeof createMockRefreshTokenRepo>; - - beforeEach(() => { - vi.clearAllMocks(); - mockUserRepo = createMockUserRepo(); - mockRefreshTokenRepo = createMockRefreshTokenRepo(); - const auth = createAuthRouter({ - userRepo: mockUserRepo, - refreshTokenRepo: mockRefreshTokenRepo, - }); - app = new Hono(); - app.onError(errorHandler); - app.route("/api/auth", auth); - }); - - it("creates a new user with valid credentials", async () => { - vi.mocked(mockUserRepo.existsByUsername).mockResolvedValue(false); - vi.mocked(mockUserRepo.create).mockResolvedValue({ - id: "test-uuid-123", - username: "testuser", - createdAt: new Date("2024-01-01T00:00:00Z"), - } as UserPublic); - - const res = await app.request("/api/auth/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - username: "testuser", - password: "securepassword12345", - }), - }); - - expect(res.status).toBe(201); - const body = (await res.json()) as RegisterResponse; - expect(body.user).toEqual({ - id: "test-uuid-123", - username: "testuser", - createdAt: "2024-01-01T00:00:00.000Z", - }); - expect(mockUserRepo.existsByUsername).toHaveBeenCalledWith("testuser"); - expect(mockUserRepo.create).toHaveBeenCalledWith({ - username: "testuser", - passwordHash: "hashed_securepassword12345", - }); - }); - - it("returns 422 for invalid username", async () => { - const res = await app.request("/api/auth/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - username: "", - password: "securepassword12345", - }), - }); - - expect(res.status).toBe(422); - const body = (await res.json()) as RegisterResponse; - expect(body.error?.code).toBe("VALIDATION_ERROR"); - }); - - it("returns 422 for password too short", async () => { - const res = await app.request("/api/auth/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - username: "testuser", - password: "tooshort123456", - }), - }); - - expect(res.status).toBe(422); - const body = (await res.json()) as RegisterResponse; - expect(body.error?.code).toBe("VALIDATION_ERROR"); - }); - - it("returns 409 for existing username", async () => { - vi.mocked(mockUserRepo.existsByUsername).mockResolvedValue(true); - - const res = await app.request("/api/auth/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - username: "existinguser", - password: "securepassword12345", - }), - }); - - expect(res.status).toBe(409); - const body = (await res.json()) as RegisterResponse; - expect(body.error?.code).toBe("USERNAME_EXISTS"); - }); -}); - -describe("POST /login", () => { - let app: Hono; - let mockUserRepo: ReturnType<typeof createMockUserRepo>; - let mockRefreshTokenRepo: ReturnType<typeof createMockRefreshTokenRepo>; - - beforeEach(() => { - vi.clearAllMocks(); - mockUserRepo = createMockUserRepo(); - mockRefreshTokenRepo = createMockRefreshTokenRepo(); - const auth = createAuthRouter({ - userRepo: mockUserRepo, - refreshTokenRepo: mockRefreshTokenRepo, - }); - app = new Hono(); - app.onError(errorHandler); - app.route("/api/auth", auth); - }); - - it("returns access token for valid credentials", async () => { - vi.mocked(mockUserRepo.findByUsername).mockResolvedValue({ - id: "user-uuid-123", - username: "testuser", - passwordHash: "hashed_correctpassword", - }); - vi.mocked(mockRefreshTokenRepo.create).mockResolvedValue(undefined); - - const res = await app.request("/api/auth/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - username: "testuser", - password: "correctpassword", - }), - }); - - expect(res.status).toBe(200); - const body = (await res.json()) as LoginResponse; - expect(body.accessToken).toBeDefined(); - expect(typeof body.accessToken).toBe("string"); - expect(body.refreshToken).toBeDefined(); - expect(typeof body.refreshToken).toBe("string"); - expect(body.user).toEqual({ - id: "user-uuid-123", - username: "testuser", - }); - expect(mockRefreshTokenRepo.create).toHaveBeenCalledWith({ - userId: "user-uuid-123", - tokenHash: expect.any(String), - expiresAt: expect.any(Date), - }); - }); - - it("returns 401 for non-existent user", async () => { - vi.mocked(mockUserRepo.findByUsername).mockResolvedValue(undefined); - - const res = await app.request("/api/auth/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - username: "nonexistent", - password: "anypassword", - }), - }); - - expect(res.status).toBe(401); - const body = (await res.json()) as LoginResponse; - expect(body.error?.code).toBe("INVALID_CREDENTIALS"); - }); - - it("returns 401 for incorrect password", async () => { - vi.mocked(mockUserRepo.findByUsername).mockResolvedValue({ - id: "user-uuid-123", - username: "testuser", - passwordHash: "hashed_correctpassword", - }); - - const res = await app.request("/api/auth/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - username: "testuser", - password: "wrongpassword", - }), - }); - - expect(res.status).toBe(401); - const body = (await res.json()) as LoginResponse; - expect(body.error?.code).toBe("INVALID_CREDENTIALS"); - }); - - it("returns 422 for missing username", async () => { - const res = await app.request("/api/auth/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - username: "", - password: "somepassword", - }), - }); - - expect(res.status).toBe(422); - const body = (await res.json()) as LoginResponse; - expect(body.error?.code).toBe("VALIDATION_ERROR"); - }); - - it("returns 422 for missing password", async () => { - const res = await app.request("/api/auth/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - username: "testuser", - password: "", - }), - }); - - expect(res.status).toBe(422); - const body = (await res.json()) as LoginResponse; - expect(body.error?.code).toBe("VALIDATION_ERROR"); - }); -}); - -interface RefreshResponse { - accessToken?: string; - refreshToken?: string; - user?: { - id: string; - username: string; - }; - error?: { - code: string; - message: string; - }; -} - -describe("POST /refresh", () => { - let app: Hono; - let mockUserRepo: ReturnType<typeof createMockUserRepo>; - let mockRefreshTokenRepo: ReturnType<typeof createMockRefreshTokenRepo>; - - beforeEach(() => { - vi.clearAllMocks(); - mockUserRepo = createMockUserRepo(); - mockRefreshTokenRepo = createMockRefreshTokenRepo(); - const auth = createAuthRouter({ - userRepo: mockUserRepo, - refreshTokenRepo: mockRefreshTokenRepo, - }); - app = new Hono(); - app.onError(errorHandler); - app.route("/api/auth", auth); - }); - - it("returns new tokens for valid refresh token", async () => { - vi.mocked(mockRefreshTokenRepo.findValidToken).mockResolvedValue({ - id: "token-id-123", - userId: "user-uuid-123", - expiresAt: new Date(Date.now() + 86400000), - }); - vi.mocked(mockUserRepo.findById).mockResolvedValue({ - id: "user-uuid-123", - username: "testuser", - }); - vi.mocked(mockRefreshTokenRepo.deleteById).mockResolvedValue(undefined); - vi.mocked(mockRefreshTokenRepo.create).mockResolvedValue(undefined); - - const res = await app.request("/api/auth/refresh", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - refreshToken: "valid-refresh-token-hex", - }), - }); - - expect(res.status).toBe(200); - const body = (await res.json()) as RefreshResponse; - expect(body.accessToken).toBeDefined(); - expect(typeof body.accessToken).toBe("string"); - expect(body.refreshToken).toBeDefined(); - expect(typeof body.refreshToken).toBe("string"); - expect(body.user).toEqual({ - id: "user-uuid-123", - username: "testuser", - }); - expect(mockRefreshTokenRepo.deleteById).toHaveBeenCalledWith( - "token-id-123", - ); - expect(mockRefreshTokenRepo.create).toHaveBeenCalledWith({ - userId: "user-uuid-123", - tokenHash: expect.any(String), - expiresAt: expect.any(Date), - }); - }); - - it("returns 401 for invalid refresh token", async () => { - vi.mocked(mockRefreshTokenRepo.findValidToken).mockResolvedValue(undefined); - - const res = await app.request("/api/auth/refresh", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - refreshToken: "invalid-refresh-token", - }), - }); - - expect(res.status).toBe(401); - const body = (await res.json()) as RefreshResponse; - expect(body.error?.code).toBe("INVALID_REFRESH_TOKEN"); - }); - - it("returns 401 for expired refresh token", async () => { - vi.mocked(mockRefreshTokenRepo.findValidToken).mockResolvedValue(undefined); - - const res = await app.request("/api/auth/refresh", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - refreshToken: "expired-refresh-token", - }), - }); - - expect(res.status).toBe(401); - const body = (await res.json()) as RefreshResponse; - expect(body.error?.code).toBe("INVALID_REFRESH_TOKEN"); - }); - - it("returns 401 when user not found", async () => { - vi.mocked(mockRefreshTokenRepo.findValidToken).mockResolvedValue({ - id: "token-id-123", - userId: "deleted-user-id", - expiresAt: new Date(Date.now() + 86400000), - }); - vi.mocked(mockUserRepo.findById).mockResolvedValue(undefined); - - const res = await app.request("/api/auth/refresh", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - refreshToken: "valid-refresh-token", - }), - }); - - expect(res.status).toBe(401); - const body = (await res.json()) as RefreshResponse; - expect(body.error?.code).toBe("USER_NOT_FOUND"); - }); - - it("returns 422 for missing refresh token", async () => { - const res = await app.request("/api/auth/refresh", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}), - }); - - expect(res.status).toBe(422); - const body = (await res.json()) as RefreshResponse; - expect(body.error?.code).toBe("VALIDATION_ERROR"); - }); - - it("returns 422 for empty refresh token", async () => { - const res = await app.request("/api/auth/refresh", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - refreshToken: "", - }), - }); - - expect(res.status).toBe(422); - const body = (await res.json()) as RefreshResponse; - expect(body.error?.code).toBe("VALIDATION_ERROR"); - }); -}); diff --git a/pkgs/server/src/routes/auth.ts b/pkgs/server/src/routes/auth.ts deleted file mode 100644 index e1f7ebb..0000000 --- a/pkgs/server/src/routes/auth.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { createHash, randomBytes } from "node:crypto"; -import { - createUserSchema, - loginSchema, - refreshTokenSchema, -} from "@kioku/shared"; -import * as argon2 from "argon2"; -import { Hono } from "hono"; -import { sign } from "hono/jwt"; -import { Errors } from "../middleware"; -import { - type RefreshTokenRepository, - refreshTokenRepository, - type UserRepository, - userRepository, -} from "../repositories"; - -const JWT_SECRET = process.env.JWT_SECRET; -if (!JWT_SECRET) { - throw new Error("JWT_SECRET environment variable is required"); -} -const ACCESS_TOKEN_EXPIRES_IN = 60 * 15; // 15 minutes -const REFRESH_TOKEN_EXPIRES_IN = 60 * 60 * 24 * 7; // 7 days - -function generateRefreshToken(): string { - return randomBytes(32).toString("hex"); -} - -function hashToken(token: string): string { - return createHash("sha256").update(token).digest("hex"); -} - -export interface AuthDependencies { - userRepo: UserRepository; - refreshTokenRepo: RefreshTokenRepository; -} - -export function createAuthRouter(deps: AuthDependencies) { - const { userRepo, refreshTokenRepo } = deps; - const auth = new Hono(); - - auth.post("/register", async (c) => { - const body = await c.req.json(); - - const parsed = createUserSchema.safeParse(body); - if (!parsed.success) { - throw Errors.validationError(parsed.error.issues[0]?.message); - } - - const { username, password } = parsed.data; - - // Check if username already exists - const exists = await userRepo.existsByUsername(username); - if (exists) { - throw Errors.conflict("Username already exists", "USERNAME_EXISTS"); - } - - // Hash password with Argon2 - const passwordHash = await argon2.hash(password); - - // Create user - const newUser = await userRepo.create({ username, passwordHash }); - - return c.json({ user: newUser }, 201); - }); - - auth.post("/login", async (c) => { - const body = await c.req.json(); - - const parsed = loginSchema.safeParse(body); - if (!parsed.success) { - throw Errors.validationError(parsed.error.issues[0]?.message); - } - - const { username, password } = parsed.data; - - // Find user by username - const user = await userRepo.findByUsername(username); - - if (!user) { - throw Errors.unauthorized( - "Invalid username or password", - "INVALID_CREDENTIALS", - ); - } - - // Verify password - const isPasswordValid = await argon2.verify(user.passwordHash, password); - if (!isPasswordValid) { - throw Errors.unauthorized( - "Invalid username or password", - "INVALID_CREDENTIALS", - ); - } - - // Generate JWT access token - const now = Math.floor(Date.now() / 1000); - const accessToken = await sign( - { - sub: user.id, - iat: now, - exp: now + ACCESS_TOKEN_EXPIRES_IN, - }, - JWT_SECRET, - ); - - // Generate refresh token - const refreshToken = generateRefreshToken(); - const tokenHash = hashToken(refreshToken); - const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRES_IN * 1000); - - // Store refresh token in database - await refreshTokenRepo.create({ - userId: user.id, - tokenHash, - expiresAt, - }); - - return c.json({ - accessToken, - refreshToken, - user: { - id: user.id, - username: user.username, - }, - }); - }); - - auth.post("/refresh", async (c) => { - const body = await c.req.json(); - - const parsed = refreshTokenSchema.safeParse(body); - if (!parsed.success) { - throw Errors.validationError(parsed.error.issues[0]?.message); - } - - const { refreshToken } = parsed.data; - const tokenHash = hashToken(refreshToken); - - // Find valid refresh token - const storedToken = await refreshTokenRepo.findValidToken(tokenHash); - - if (!storedToken) { - throw Errors.unauthorized( - "Invalid or expired refresh token", - "INVALID_REFRESH_TOKEN", - ); - } - - // Get user info - const user = await userRepo.findById(storedToken.userId); - - if (!user) { - throw Errors.unauthorized("User not found", "USER_NOT_FOUND"); - } - - // Delete old refresh token (rotation) - await refreshTokenRepo.deleteById(storedToken.id); - - // Generate new access token - const now = Math.floor(Date.now() / 1000); - const accessToken = await sign( - { - sub: user.id, - iat: now, - exp: now + ACCESS_TOKEN_EXPIRES_IN, - }, - JWT_SECRET, - ); - - // Generate new refresh token (rotation) - const newRefreshToken = generateRefreshToken(); - const newTokenHash = hashToken(newRefreshToken); - const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRES_IN * 1000); - - await refreshTokenRepo.create({ - userId: user.id, - tokenHash: newTokenHash, - expiresAt, - }); - - return c.json({ - accessToken, - refreshToken: newRefreshToken, - user: { - id: user.id, - username: user.username, - }, - }); - }); - - return auth; -} - -// Default auth router with real repositories for production use -export const auth = createAuthRouter({ - userRepo: userRepository, - refreshTokenRepo: refreshTokenRepository, -}); diff --git a/pkgs/server/src/routes/index.ts b/pkgs/server/src/routes/index.ts deleted file mode 100644 index 2925e6d..0000000 --- a/pkgs/server/src/routes/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { auth } from "./auth"; diff --git a/pkgs/server/tsconfig.json b/pkgs/server/tsconfig.json deleted file mode 100644 index 038af8a..0000000 --- a/pkgs/server/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/pkgs/server/vitest.config.ts b/pkgs/server/vitest.config.ts deleted file mode 100644 index af9649f..0000000 --- a/pkgs/server/vitest.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - globals: true, - env: { - JWT_SECRET: "test-secret-key", - }, - }, -}); |
