Adding totp enabled to session, restarting migrations, updating many Shadcn svelte components, and refactoring to move 2FA to its own page.

This commit is contained in:
Bradley Shellnut 2024-06-08 15:09:21 -07:00
parent a8eed0d86d
commit 2609c28619
98 changed files with 5757 additions and 21633 deletions

2
.nvmrc
View file

@ -1 +1 @@
v20
20

View file

@ -2,9 +2,9 @@ import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'postgresql',
schema: './src/db/schema/index.ts',
out: './src/db/migrations',
driver: 'pg',
dbCredentials: {
host: process.env.DATABASE_HOST || 'localhost',
port: Number(process.env.DATABASE_PORT) || 5432,

View file

@ -15,59 +15,59 @@
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write .",
"site:update": "pnpm update -i -L",
"generate": "drizzle-kit generate:pg",
"generate": "drizzle-kit generate",
"migrate": "tsx src/db/migrate.ts",
"seed": "tsx src/db/seed.ts",
"push": "drizzle-kit push:pg"
"push": "drizzle-kit push"
},
"devDependencies": {
"@melt-ui/pp": "^0.3.1",
"@melt-ui/svelte": "^0.77.0",
"@playwright/test": "^1.43.1",
"@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.81.0",
"@playwright/test": "^1.44.1",
"@resvg/resvg-js": "^2.6.2",
"@sveltejs/adapter-auto": "^3.2.0",
"@sveltejs/enhanced-img": "^0.2.0",
"@sveltejs/kit": "^2.5.7",
"@sveltejs/vite-plugin-svelte": "^3.1.0",
"@sveltejs/adapter-auto": "^3.2.1",
"@sveltejs/enhanced-img": "^0.2.1",
"@sveltejs/kit": "^2.5.10",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@types/cookie": "^0.6.0",
"@types/node": "^20.12.6",
"@types/pg": "^8.11.5",
"@typescript-eslint/eslint-plugin": "^7.7.1",
"@typescript-eslint/parser": "^7.7.1",
"@types/node": "^20.14.2",
"@types/pg": "^8.11.6",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"autoprefixer": "^10.4.19",
"dotenv": "^16.4.5",
"drizzle-kit": "^0.20.17",
"drizzle-kit": "^0.22.4",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.38.0",
"eslint-plugin-svelte": "^2.39.0",
"just-clone": "^6.2.0",
"just-debounce-it": "^3.2.0",
"postcss": "^8.4.38",
"postcss-import": "^16.1.0",
"postcss-load-config": "^5.0.3",
"postcss-preset-env": "^9.5.9",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.3",
"sass": "^1.74.1",
"postcss-load-config": "^5.1.0",
"postcss-preset-env": "^9.5.14",
"prettier": "^3.3.1",
"prettier-plugin-svelte": "^3.2.4",
"sass": "^1.77.4",
"satori": "^0.10.13",
"satori-html": "^0.3.2",
"svelte": "^4.2.15",
"svelte-check": "^3.7.0",
"svelte": "^4.2.18",
"svelte-check": "^3.8.0",
"svelte-headless-table": "^0.18.2",
"svelte-meta-tags": "^3.1.2",
"svelte-preprocess": "^5.1.4",
"svelte-sequential-preprocessor": "^2.0.1",
"sveltekit-flash-message": "^2.4.4",
"sveltekit-rate-limiter": "^0.5.1",
"sveltekit-superforms": "^2.12.6",
"tailwindcss": "^3.4.3",
"sveltekit-superforms": "^2.14.0",
"tailwindcss": "^3.4.4",
"ts-node": "^10.9.2",
"tslib": "^2.6.1",
"tsx": "^4.7.3",
"typescript": "^5.4.4",
"vite": "^5.2.10",
"vitest": "^1.5.2",
"zod": "^3.23.4"
"tslib": "^2.6.3",
"tsx": "^4.12.0",
"typescript": "^5.4.5",
"vite": "^5.2.12",
"vitest": "^1.6.0",
"zod": "^3.23.8"
},
"type": "module",
"engines": {
@ -76,22 +76,22 @@
},
"dependencies": {
"@fontsource/fira-mono": "^5.0.13",
"@iconify-icons/line-md": "^1.2.26",
"@iconify-icons/mdi": "^1.2.47",
"@iconify-icons/line-md": "^1.2.30",
"@iconify-icons/mdi": "^1.2.48",
"@lucia-auth/adapter-drizzle": "^1.0.7",
"@lukeed/uuid": "^2.0.1",
"@neondatabase/serverless": "^0.9.1",
"@neondatabase/serverless": "^0.9.3",
"@paralleldrive/cuid2": "^2.2.2",
"@sveltejs/adapter-vercel": "^5.3.0",
"@sveltejs/adapter-vercel": "^5.3.1",
"@types/feather-icons": "^4.29.4",
"@vercel/og": "^0.5.20",
"bits-ui": "^0.21.5",
"bits-ui": "^0.21.10",
"boardgamegeekclient": "^1.9.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cookie": "^0.6.0",
"drizzle-orm": "^0.30.9",
"feather-icons": "^4.29.1",
"drizzle-orm": "^0.31.1",
"feather-icons": "^4.29.2",
"formsnap": "^1.0.0",
"html-entities": "^2.5.2",
"iconify-icon": "^2.1.0",
@ -99,10 +99,10 @@
"just-kebab-case": "^4.2.0",
"loader": "^2.1.1",
"lucia": "3.2.0",
"lucide-svelte": "^0.373.0",
"lucide-svelte": "^0.390.0",
"open-props": "^1.7.4",
"oslo": "^1.2.0",
"pg": "^8.11.5",
"pg": "^8.12.0",
"postgres": "^3.4.4",
"qrcode": "^1.5.3",
"radix-svelte": "^0.9.0",
@ -110,7 +110,7 @@
"svelte-lazy-loader": "^1.0.0",
"tailwind-merge": "^2.3.0",
"tailwind-variants": "^0.2.1",
"tailwindcss-animate": "^1.0.6",
"tailwindcss-animate": "^1.0.7",
"zod-to-json-schema": "^3.23.0"
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,5 @@
DO $$ BEGIN
CREATE TYPE "external_id_type" AS ENUM('game', 'category', 'mechanic', 'publisher', 'designer', 'artist');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "categories" (
"id" uuid PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"name" text,
"slug" text,
@ -27,10 +21,10 @@ CREATE TABLE IF NOT EXISTS "categories_to_games" (
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "collection_items" (
"id" uuid PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"collection_id" text NOT NULL,
"game_id" text NOT NULL,
"collection_id" uuid NOT NULL,
"game_id" uuid NOT NULL,
"times_played" integer DEFAULT 0,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
@ -38,26 +32,17 @@ CREATE TABLE IF NOT EXISTS "collection_items" (
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "collections" (
"id" uuid PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"user_id" text NOT NULL,
"user_id" uuid NOT NULL,
"name" text DEFAULT 'My Collection' NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "collections_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "expansions" (
"id" uuid PRIMARY KEY NOT NULL,
"cuid" text,
"base_game_id" text NOT NULL,
"game_id" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "expansions_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "external_ids" (
"id" uuid PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"type" "external_id_type" NOT NULL,
"external_id" text NOT NULL,
@ -65,7 +50,7 @@ CREATE TABLE IF NOT EXISTS "external_ids" (
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "games" (
"id" uuid PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"name" text,
"slug" text,
@ -80,8 +65,7 @@ CREATE TABLE IF NOT EXISTS "games" (
"image_url" text,
"thumb_url" text,
"url" text,
"text_searchable_index" "tsvector",
"last_sync_at" timestamp (6) with time zone,
"last_sync_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "games_cuid_unique" UNIQUE("cuid")
@ -94,7 +78,7 @@ CREATE TABLE IF NOT EXISTS "games_to_external_ids" (
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "mechanics" (
"id" uuid PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"name" text,
"slug" text,
@ -117,13 +101,13 @@ CREATE TABLE IF NOT EXISTS "mechanics_to_games" (
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "password_reset_tokens" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"expires_at" timestamp (6) with time zone,
"user_id" uuid NOT NULL,
"expires_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "publishers" (
"id" uuid PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"name" text,
"slug" text,
@ -144,34 +128,46 @@ CREATE TABLE IF NOT EXISTS "publishers_to_games" (
CONSTRAINT "publishers_to_games_publisher_id_game_id_pk" PRIMARY KEY("publisher_id","game_id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "recovery_codes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"code" text NOT NULL,
"used" boolean DEFAULT false,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "roles" (
"id" uuid PRIMARY KEY NOT NULL,
"cuid" text,
"name" text,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text NOT NULL,
"name" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "roles_cuid_unique" UNIQUE("cuid"),
CONSTRAINT "roles_name_unique" UNIQUE("name")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "sessions" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"user_id" uuid NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"ip_country" text,
"ip_address" text
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "user_roles" (
"id" uuid PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"user_id" text NOT NULL,
"role_id" text NOT NULL,
"user_id" uuid NOT NULL,
"role_id" uuid NOT NULL,
"primary" boolean DEFAULT false,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "user_roles_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "users" (
"id" uuid PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"username" text,
"hashed_password" text,
@ -181,6 +177,8 @@ CREATE TABLE IF NOT EXISTS "users" (
"verified" boolean DEFAULT false,
"receive_email" boolean DEFAULT false,
"theme" text DEFAULT 'system',
"two_factor_secret" text DEFAULT '',
"two_factor_enabled" boolean DEFAULT false,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "users_cuid_unique" UNIQUE("cuid"),
@ -189,177 +187,176 @@ CREATE TABLE IF NOT EXISTS "users" (
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "wishlist_items" (
"id" uuid PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"wishlist_id" text NOT NULL,
"game_id" text NOT NULL,
"wishlist_id" uuid NOT NULL,
"game_id" uuid NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "wishlist_items_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "wishlists" (
"id" uuid PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cuid" text,
"user_id" text NOT NULL,
"user_id" uuid NOT NULL,
"name" text DEFAULT 'My Wishlist' NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "wishlists_cuid_unique" UNIQUE("cuid")
);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "text_searchable_idx" ON "games" ("text_searchable_index");--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "categories_to_external_ids" ADD CONSTRAINT "categories_to_external_ids_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "categories"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "categories_to_external_ids" ADD CONSTRAINT "categories_to_external_ids_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "categories_to_external_ids" ADD CONSTRAINT "categories_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "external_ids"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "categories_to_external_ids" ADD CONSTRAINT "categories_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "public"."external_ids"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "categories_to_games" ADD CONSTRAINT "categories_to_games_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "categories"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "categories_to_games" ADD CONSTRAINT "categories_to_games_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "categories_to_games" ADD CONSTRAINT "categories_to_games_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "categories_to_games" ADD CONSTRAINT "categories_to_games_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "collection_items" ADD CONSTRAINT "collection_items_collection_id_collections_id_fk" FOREIGN KEY ("collection_id") REFERENCES "collections"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "collection_items" ADD CONSTRAINT "collection_items_collection_id_collections_id_fk" FOREIGN KEY ("collection_id") REFERENCES "public"."collections"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "collection_items" ADD CONSTRAINT "collection_items_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "collection_items" ADD CONSTRAINT "collection_items_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "collections" ADD CONSTRAINT "collections_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "collections" ADD CONSTRAINT "collections_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "expansions" ADD CONSTRAINT "expansions_base_game_id_games_id_fk" FOREIGN KEY ("base_game_id") REFERENCES "games"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "games_to_external_ids" ADD CONSTRAINT "games_to_external_ids_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "expansions" ADD CONSTRAINT "expansions_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "games_to_external_ids" ADD CONSTRAINT "games_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "public"."external_ids"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "games_to_external_ids" ADD CONSTRAINT "games_to_external_ids_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "mechanics_to_external_ids" ADD CONSTRAINT "mechanics_to_external_ids_mechanic_id_mechanics_id_fk" FOREIGN KEY ("mechanic_id") REFERENCES "public"."mechanics"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "games_to_external_ids" ADD CONSTRAINT "games_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "external_ids"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "mechanics_to_external_ids" ADD CONSTRAINT "mechanics_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "public"."external_ids"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mechanics_to_external_ids" ADD CONSTRAINT "mechanics_to_external_ids_mechanic_id_mechanics_id_fk" FOREIGN KEY ("mechanic_id") REFERENCES "mechanics"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "mechanics_to_games" ADD CONSTRAINT "mechanics_to_games_mechanic_id_mechanics_id_fk" FOREIGN KEY ("mechanic_id") REFERENCES "public"."mechanics"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mechanics_to_external_ids" ADD CONSTRAINT "mechanics_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "external_ids"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "mechanics_to_games" ADD CONSTRAINT "mechanics_to_games_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mechanics_to_games" ADD CONSTRAINT "mechanics_to_games_mechanic_id_mechanics_id_fk" FOREIGN KEY ("mechanic_id") REFERENCES "mechanics"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "password_reset_tokens" ADD CONSTRAINT "password_reset_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mechanics_to_games" ADD CONSTRAINT "mechanics_to_games_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "publishers_to_external_ids" ADD CONSTRAINT "publishers_to_external_ids_publisher_id_publishers_id_fk" FOREIGN KEY ("publisher_id") REFERENCES "public"."publishers"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "password_reset_tokens" ADD CONSTRAINT "password_reset_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "publishers_to_external_ids" ADD CONSTRAINT "publishers_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "public"."external_ids"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "publishers_to_external_ids" ADD CONSTRAINT "publishers_to_external_ids_publisher_id_publishers_id_fk" FOREIGN KEY ("publisher_id") REFERENCES "publishers"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "publishers_to_games" ADD CONSTRAINT "publishers_to_games_publisher_id_publishers_id_fk" FOREIGN KEY ("publisher_id") REFERENCES "public"."publishers"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "publishers_to_external_ids" ADD CONSTRAINT "publishers_to_external_ids_external_id_external_ids_id_fk" FOREIGN KEY ("external_id") REFERENCES "external_ids"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "publishers_to_games" ADD CONSTRAINT "publishers_to_games_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE restrict ON UPDATE cascade;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "publishers_to_games" ADD CONSTRAINT "publishers_to_games_publisher_id_publishers_id_fk" FOREIGN KEY ("publisher_id") REFERENCES "publishers"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "recovery_codes" ADD CONSTRAINT "recovery_codes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "publishers_to_games" ADD CONSTRAINT "publishers_to_games_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE restrict ON UPDATE cascade;
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "wishlist_items" ADD CONSTRAINT "wishlist_items_wishlist_id_wishlists_id_fk" FOREIGN KEY ("wishlist_id") REFERENCES "public"."wishlists"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "wishlist_items" ADD CONSTRAINT "wishlist_items_wishlist_id_wishlists_id_fk" FOREIGN KEY ("wishlist_id") REFERENCES "wishlists"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "wishlist_items" ADD CONSTRAINT "wishlist_items_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "public"."games"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "wishlist_items" ADD CONSTRAINT "wishlist_items_game_id_games_id_fk" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "wishlists" ADD CONSTRAINT "wishlists_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "wishlists" ADD CONSTRAINT "wishlists_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE INDEX IF NOT EXISTS "search_index" ON "games" USING gin ((
setweight(to_tsvector('english', "name"), 'A') ||
setweight(to_tsvector('english', "slug"), 'B')
));

View file

@ -1,6 +0,0 @@
ALTER TABLE "collection_items" ALTER COLUMN "collection_id" SET DATA TYPE uuid;--> statement-breakpoint
ALTER TABLE "collection_items" ALTER COLUMN "game_id" SET DATA TYPE uuid;--> statement-breakpoint
ALTER TABLE "expansions" ALTER COLUMN "base_game_id" SET DATA TYPE uuid;--> statement-breakpoint
ALTER TABLE "expansions" ALTER COLUMN "game_id" SET DATA TYPE uuid;--> statement-breakpoint
ALTER TABLE "wishlist_items" ALTER COLUMN "wishlist_id" SET DATA TYPE uuid;--> statement-breakpoint
ALTER TABLE "wishlist_items" ALTER COLUMN "game_id" SET DATA TYPE uuid;

View file

@ -1,5 +0,0 @@
ALTER TABLE "collections" ALTER COLUMN "user_id" SET DATA TYPE uuid;--> statement-breakpoint
ALTER TABLE "password_reset_tokens" ALTER COLUMN "user_id" SET DATA TYPE uuid;--> statement-breakpoint
ALTER TABLE "sessions" ALTER COLUMN "user_id" SET DATA TYPE uuid;--> statement-breakpoint
ALTER TABLE "user_roles" ALTER COLUMN "user_id" SET DATA TYPE uuid;--> statement-breakpoint
ALTER TABLE "wishlists" ALTER COLUMN "user_id" SET DATA TYPE uuid;

View file

@ -1 +0,0 @@
ALTER TABLE "user_roles" ALTER COLUMN "role_id" SET DATA TYPE uuid;

View file

@ -1,13 +0,0 @@
ALTER TABLE "categories" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
ALTER TABLE "collection_items" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
ALTER TABLE "collections" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
ALTER TABLE "expansions" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
ALTER TABLE "external_ids" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
ALTER TABLE "games" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
ALTER TABLE "mechanics" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
ALTER TABLE "publishers" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
ALTER TABLE "roles" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
ALTER TABLE "user_roles" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
ALTER TABLE "wishlist_items" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
ALTER TABLE "wishlists" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();

View file

@ -1,2 +0,0 @@
ALTER TABLE "roles" ALTER COLUMN "cuid" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "roles" ALTER COLUMN "name" SET NOT NULL;

View file

@ -1 +0,0 @@
ALTER TABLE "user_roles" ADD COLUMN "default" boolean DEFAULT false;

View file

@ -1 +0,0 @@
ALTER TABLE "user_roles" RENAME COLUMN "default" TO "primary";

View file

@ -1 +0,0 @@
ALTER TABLE "users" ADD COLUMN "two_factor_secret" text;

View file

@ -1,2 +0,0 @@
ALTER TABLE "users" ALTER COLUMN "two_factor_secret" SET DEFAULT '';--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "two_factor_enabled" boolean DEFAULT false;

View file

@ -1,14 +0,0 @@
CREATE TABLE IF NOT EXISTS "recovery_codes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"code" text NOT NULL,
"used" boolean DEFAULT false,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "recovery_codes" ADD CONSTRAINT "recovery_codes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -1,2 +0,0 @@
ALTER TABLE "collections" ADD COLUMN "name" text DEFAULT 'My Collection' NOT NULL;--> statement-breakpoint
ALTER TABLE "wishlists" ADD COLUMN "name" text DEFAULT 'My Wishlist' NOT NULL;

View file

@ -1,10 +1,10 @@
{
"id": "176e5b1f-d146-4604-ab17-b16052da13c0",
"id": "9622fc3a-51a1-4f66-8535-fd1ceaf46fc2",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "5",
"dialect": "pg",
"version": "7",
"dialect": "postgresql",
"tables": {
"categories": {
"public.categories": {
"name": "categories",
"schema": "",
"columns": {
@ -12,7 +12,8 @@
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"cuid": {
"name": "cuid",
@ -60,7 +61,7 @@
}
}
},
"categories_to_external_ids": {
"public.categories_to_external_ids": {
"name": "categories_to_external_ids",
"schema": "",
"columns": {
@ -117,7 +118,7 @@
},
"uniqueConstraints": {}
},
"categories_to_games": {
"public.categories_to_games": {
"name": "categories_to_games",
"schema": "",
"columns": {
@ -174,7 +175,7 @@
},
"uniqueConstraints": {}
},
"collection_items": {
"public.collection_items": {
"name": "collection_items",
"schema": "",
"columns": {
@ -182,7 +183,8 @@
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"cuid": {
"name": "cuid",
@ -192,13 +194,13 @@
},
"collection_id": {
"name": "collection_id",
"type": "text",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"game_id": {
"name": "game_id",
"type": "text",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
@ -264,7 +266,7 @@
}
}
},
"collections": {
"public.collections": {
"name": "collections",
"schema": "",
"columns": {
@ -272,7 +274,8 @@
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"cuid": {
"name": "cuid",
@ -282,10 +285,17 @@
},
"user_id": {
"name": "user_id",
"type": "text",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'My Collection'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
@ -328,90 +338,7 @@
}
}
},
"expansions": {
"name": "expansions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"cuid": {
"name": "cuid",
"type": "text",
"primaryKey": false,
"notNull": false
},
"base_game_id": {
"name": "base_game_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"game_id": {
"name": "game_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"expansions_base_game_id_games_id_fk": {
"name": "expansions_base_game_id_games_id_fk",
"tableFrom": "expansions",
"tableTo": "games",
"columnsFrom": [
"base_game_id"
],
"columnsTo": [
"id"
],
"onDelete": "restrict",
"onUpdate": "cascade"
},
"expansions_game_id_games_id_fk": {
"name": "expansions_game_id_games_id_fk",
"tableFrom": "expansions",
"tableTo": "games",
"columnsFrom": [
"game_id"
],
"columnsTo": [
"id"
],
"onDelete": "restrict",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"expansions_cuid_unique": {
"name": "expansions_cuid_unique",
"nullsNotDistinct": false,
"columns": [
"cuid"
]
}
}
},
"external_ids": {
"public.external_ids": {
"name": "external_ids",
"schema": "",
"columns": {
@ -419,7 +346,8 @@
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"cuid": {
"name": "cuid",
@ -430,6 +358,7 @@
"type": {
"name": "type",
"type": "external_id_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
@ -453,7 +382,7 @@
}
}
},
"games": {
"public.games": {
"name": "games",
"schema": "",
"columns": {
@ -461,7 +390,8 @@
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"cuid": {
"name": "cuid",
@ -547,15 +477,9 @@
"primaryKey": false,
"notNull": false
},
"text_searchable_index": {
"name": "text_searchable_index",
"type": "tsvector",
"primaryKey": false,
"notNull": false
},
"last_sync_at": {
"name": "last_sync_at",
"type": "timestamp (6) with time zone",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
@ -575,12 +499,20 @@
}
},
"indexes": {
"text_searchable_idx": {
"name": "text_searchable_idx",
"search_index": {
"name": "search_index",
"columns": [
"text_searchable_index"
{
"expression": "(\n\t\t\t\t\t\tsetweight(to_tsvector('english', \"name\"), 'A') ||\n\t\t\t\t\t\tsetweight(to_tsvector('english', \"slug\"), 'B')\n\t\t\t\t\t)",
"asc": true,
"isExpression": true,
"nulls": "last"
}
],
"isUnique": false
"isUnique": false,
"concurrently": false,
"method": "gin",
"with": {}
}
},
"foreignKeys": {},
@ -595,7 +527,7 @@
}
}
},
"games_to_external_ids": {
"public.games_to_external_ids": {
"name": "games_to_external_ids",
"schema": "",
"columns": {
@ -652,7 +584,7 @@
},
"uniqueConstraints": {}
},
"mechanics": {
"public.mechanics": {
"name": "mechanics",
"schema": "",
"columns": {
@ -660,7 +592,8 @@
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"cuid": {
"name": "cuid",
@ -708,7 +641,7 @@
}
}
},
"mechanics_to_external_ids": {
"public.mechanics_to_external_ids": {
"name": "mechanics_to_external_ids",
"schema": "",
"columns": {
@ -765,7 +698,7 @@
},
"uniqueConstraints": {}
},
"mechanics_to_games": {
"public.mechanics_to_games": {
"name": "mechanics_to_games",
"schema": "",
"columns": {
@ -822,7 +755,7 @@
},
"uniqueConstraints": {}
},
"password_reset_tokens": {
"public.password_reset_tokens": {
"name": "password_reset_tokens",
"schema": "",
"columns": {
@ -834,13 +767,13 @@
},
"user_id": {
"name": "user_id",
"type": "text",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp (6) with time zone",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
@ -871,7 +804,7 @@
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"publishers": {
"public.publishers": {
"name": "publishers",
"schema": "",
"columns": {
@ -879,7 +812,8 @@
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"cuid": {
"name": "cuid",
@ -927,7 +861,7 @@
}
}
},
"publishers_to_external_ids": {
"public.publishers_to_external_ids": {
"name": "publishers_to_external_ids",
"schema": "",
"columns": {
@ -984,7 +918,7 @@
},
"uniqueConstraints": {}
},
"publishers_to_games": {
"public.publishers_to_games": {
"name": "publishers_to_games",
"schema": "",
"columns": {
@ -1041,7 +975,71 @@
},
"uniqueConstraints": {}
},
"roles": {
"public.recovery_codes": {
"name": "recovery_codes",
"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
},
"code": {
"name": "code",
"type": "text",
"primaryKey": false,
"notNull": true
},
"used": {
"name": "used",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"recovery_codes_user_id_users_id_fk": {
"name": "recovery_codes_user_id_users_id_fk",
"tableFrom": "recovery_codes",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.roles": {
"name": "roles",
"schema": "",
"columns": {
@ -1049,19 +1047,34 @@
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"cuid": {
"name": "cuid",
"type": "text",
"primaryKey": false,
"notNull": false
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
@ -1084,7 +1097,7 @@
}
}
},
"sessions": {
"public.sessions": {
"name": "sessions",
"schema": "",
"columns": {
@ -1096,7 +1109,7 @@
},
"user_id": {
"name": "user_id",
"type": "text",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
@ -1138,7 +1151,7 @@
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user_roles": {
"public.user_roles": {
"name": "user_roles",
"schema": "",
"columns": {
@ -1146,7 +1159,8 @@
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"cuid": {
"name": "cuid",
@ -1156,16 +1170,23 @@
},
"user_id": {
"name": "user_id",
"type": "text",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role_id": {
"name": "role_id",
"type": "text",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"primary": {
"name": "primary",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
@ -1221,7 +1242,7 @@
}
}
},
"users": {
"public.users": {
"name": "users",
"schema": "",
"columns": {
@ -1229,7 +1250,8 @@
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"cuid": {
"name": "cuid",
@ -1288,6 +1310,20 @@
"notNull": false,
"default": "'system'"
},
"two_factor_secret": {
"name": "two_factor_secret",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "''"
},
"two_factor_enabled": {
"name": "two_factor_enabled",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
@ -1330,7 +1366,7 @@
}
}
},
"wishlist_items": {
"public.wishlist_items": {
"name": "wishlist_items",
"schema": "",
"columns": {
@ -1338,7 +1374,8 @@
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"cuid": {
"name": "cuid",
@ -1348,13 +1385,13 @@
},
"wishlist_id": {
"name": "wishlist_id",
"type": "text",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"game_id": {
"name": "game_id",
"type": "text",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
@ -1413,7 +1450,7 @@
}
}
},
"wishlists": {
"public.wishlists": {
"name": "wishlists",
"schema": "",
"columns": {
@ -1421,7 +1458,8 @@
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"cuid": {
"name": "cuid",
@ -1431,10 +1469,17 @@
},
"user_id": {
"name": "user_id",
"type": "text",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'My Wishlist'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
@ -1478,19 +1523,7 @@
}
}
},
"enums": {
"external_id_type": {
"name": "external_id_type",
"values": {
"game": "game",
"category": "category",
"mechanic": "mechanic",
"publisher": "publisher",
"designer": "designer",
"artist": "artist"
}
}
},
"enums": {},
"schemas": {},
"_meta": {
"columns": {},

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,89 +1,12 @@
{
"version": "5",
"dialect": "pg",
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1710268038944,
"tag": "0000_tricky_hitman",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1710268191378,
"tag": "0001_numerous_dragon_man",
"breakpoints": true
},
{
"idx": 2,
"version": "5",
"when": 1710268300740,
"tag": "0002_thick_lyja",
"breakpoints": true
},
{
"idx": 3,
"version": "5",
"when": 1710268371021,
"tag": "0003_mushy_madame_masque",
"breakpoints": true
},
{
"idx": 4,
"version": "5",
"when": 1710277583673,
"tag": "0004_glossy_enchantress",
"breakpoints": true
},
{
"idx": 5,
"version": "5",
"when": 1710366724519,
"tag": "0005_light_captain_marvel",
"breakpoints": true
},
{
"idx": 6,
"version": "5",
"when": 1710905422967,
"tag": "0006_wild_stone_men",
"breakpoints": true
},
{
"idx": 7,
"version": "5",
"when": 1710905572670,
"tag": "0007_large_miss_america",
"breakpoints": true
},
{
"idx": 8,
"version": "5",
"when": 1711757183163,
"tag": "0008_amusing_franklin_richards",
"breakpoints": true
},
{
"idx": 9,
"version": "5",
"when": 1711868447607,
"tag": "0009_gray_carlie_cooper",
"breakpoints": true
},
{
"idx": 10,
"version": "5",
"when": 1712271520175,
"tag": "0010_wakeful_metal_master",
"breakpoints": true
},
{
"idx": 11,
"version": "5",
"when": 1713311328819,
"tag": "0011_charming_bucky",
"version": "7",
"when": 1717548517806,
"tag": "0000_spotty_changeling",
"breakpoints": true
}
]

View file

@ -1,6 +1,7 @@
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import categoriesToExternalIds from './categoriesToExternalIds';
import categories_to_games from './categoriesToGames';
const categories = pgTable('categories', {
@ -10,8 +11,8 @@ const categories = pgTable('categories', {
.$defaultFn(() => cuid2()),
name: text('name'),
slug: text('slug'),
created_at: timestamp('created_at').notNull().defaultNow(),
updated_at: timestamp('updated_at').notNull().defaultNow(),
created_at: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
updated_at: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
});
export type Categories = InferSelectModel<typeof categories>;

View file

@ -16,8 +16,8 @@ const collection_items = pgTable('collection_items', {
.notNull()
.references(() => games.id, { onDelete: 'cascade' }),
times_played: integer('times_played').default(0),
created_at: timestamp('created_at').notNull().defaultNow(),
updated_at: timestamp('updated_at').notNull().defaultNow(),
created_at: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
updated_at: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
});
export type CollectionItems = InferSelectModel<typeof collection_items>;

View file

@ -12,8 +12,8 @@ const collections = pgTable('collections', {
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
name: text('name').notNull().default('My Collection'),
created_at: timestamp('created_at').notNull().defaultNow(),
updated_at: timestamp('updated_at').notNull().defaultNow(),
created_at: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
updated_at: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
});
export const collection_relations = relations(collections, ({ one }) => ({

View file

@ -14,8 +14,8 @@ export const expansions = pgTable('expansions', {
game_id: uuid('game_id')
.notNull()
.references(() => games.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
created_at: timestamp('created_at').notNull().defaultNow(),
updated_at: timestamp('updated_at').notNull().defaultNow(),
created_at: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
updated_at: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
});
export type Expansions = InferSelectModel<typeof expansions>;

View file

@ -1,6 +1,5 @@
import { index, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { tsvector } from '../../tsVector';
import { type InferSelectModel, relations, sql } from 'drizzle-orm';
import categoriesToGames from './categoriesToGames';
import gamesToExternalIds from './gamesToExternalIds';
@ -27,22 +26,19 @@ const games = pgTable(
image_url: text('image_url'),
thumb_url: text('thumb_url'),
url: text('url'),
text_searchable_index: tsvector('text_searchable_index'),
last_sync_at: timestamp('last_sync_at', {
withTimezone: true,
mode: 'date',
precision: 6,
}),
created_at: timestamp('created_at').notNull().defaultNow(),
updated_at: timestamp('updated_at').notNull().defaultNow(),
},
(table) => {
return {
text_searchable_idx: index('text_searchable_idx')
.on(table.text_searchable_index)
.using(sql`'gin'`),
};
last_sync_at: timestamp('last_sync_at'),
created_at: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
updated_at: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
},
(table) => ({
searchIndex: index('search_index').using(
'gin',
sql`(
setweight(to_tsvector('english', ${table.name}), 'A') ||
setweight(to_tsvector('english', ${table.slug}), 'B')
)`,
),
}),
);
export const gameRelations = relations(games, ({ many }) => ({

View file

@ -10,13 +10,13 @@ export { default as roles, role_relations, type Roles } from './roles';
export { default as userRoles, user_role_relations, type UserRoles } from './userRoles';
export { default as collections, collection_relations, type Collections } from './collections';
export {
default as collectionItems,
default as collection_items,
collection_item_relations,
type CollectionItems,
} from './collectionItems';
export { default as wishlists, wishlists_relations, type Wishlists } from './wishlists';
export {
default as wishlistItems,
default as wishlist_items,
wishlist_item_relations,
type WishlistItems,
} from './wishlistItems';
@ -24,11 +24,11 @@ export { default as externalIds, type ExternalIds, type ExternalIdType } from '.
export { default as games, gameRelations, type Games } from './games';
export { default as gamesToExternalIds } from './gamesToExternalIds';
export { default as publishers, publishers_relations, type Publishers } from './publishers';
export { default as publishersToGames, publishers_to_games_relations } from './publishersToGames';
export { default as publishers_to_games, publishers_to_games_relations } from './publishersToGames';
export { default as publishersToExternalIds } from './publishersToExternalIds';
export { default as categories, categories_relations, type Categories } from './categories';
export { default as categoriesToExternalIds } from './categoriesToExternalIds';
export { default as categoriesToGames, categories_to_games_relations } from './categoriesToGames';
export { default as categories_to_games, categories_to_games_relations } from './categoriesToGames';
export { default as mechanics, mechanics_relations, type Mechanics } from './mechanics';
export { default as mechanicsToExternalIds } from './mechanicsToExternalIds';
export { default as mechanicsToGames, mechanics_to_games_relations } from './mechanicsToGames';
export { default as mechanics_to_games, mechanics_to_games_relations } from './mechanicsToGames';

View file

@ -11,8 +11,8 @@ const mechanics = pgTable('mechanics', {
.$defaultFn(() => cuid2()),
name: text('name'),
slug: text('slug'),
created_at: timestamp('created_at').notNull().defaultNow(),
updated_at: timestamp('updated_at').notNull().defaultNow(),
created_at: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
updated_at: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
});
export type Mechanics = InferSelectModel<typeof mechanics>;

View file

@ -10,12 +10,8 @@ const password_reset_tokens = pgTable('password_reset_tokens', {
user_id: uuid('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
expires_at: timestamp('expires_at', {
withTimezone: true,
mode: 'date',
precision: 6,
}),
created_at: timestamp('created_at').notNull().defaultNow(),
expires_at: timestamp('expires_at', { mode: 'string' }),
created_at: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
});
export type PasswordResetTokens = InferSelectModel<typeof password_reset_tokens>;

View file

@ -11,8 +11,8 @@ const publishers = pgTable('publishers', {
.$defaultFn(() => cuid2()),
name: text('name'),
slug: text('slug'),
created_at: timestamp('created_at').notNull().defaultNow(),
updated_at: timestamp('updated_at').notNull().defaultNow(),
created_at: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
updated_at: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
});
export type Publishers = InferSelectModel<typeof publishers>;

View file

@ -9,8 +9,8 @@ const recovery_codes = pgTable('recovery_codes', {
.references(() => users.id),
code: text('code').notNull(),
used: boolean('used').default(false),
created_at: timestamp('created_at').notNull().defaultNow(),
updated_at: timestamp('updated_at').notNull().defaultNow(),
created_at: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
updated_at: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
});
export type RecoveryCodes = InferSelectModel<typeof recovery_codes>;

View file

@ -1,4 +1,4 @@
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import user_roles from './userRoles';
@ -10,6 +10,8 @@ const roles = pgTable('roles', {
.$defaultFn(() => cuid2())
.notNull(),
name: text('name').unique().notNull(),
created_at: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
updated_at: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
});
export type Roles = InferSelectModel<typeof roles>;

View file

@ -1,4 +1,4 @@
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import users from './users';
const sessions = pgTable('sessions', {
@ -12,6 +12,7 @@ const sessions = pgTable('sessions', {
}).notNull(),
ipCountry: text('ip_country'),
ipAddress: text('ip_address'),
isTwoFactorAuthenticated: boolean('is_two_factor_authenticated').default(false),
});
export default sessions;

View file

@ -16,8 +16,8 @@ const user_roles = pgTable('user_roles', {
.notNull()
.references(() => roles.id, { onDelete: 'cascade' }),
primary: boolean('primary').default(false),
created_at: timestamp('created_at').notNull().defaultNow(),
updated_at: timestamp('updated_at').notNull().defaultNow(),
created_at: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
updated_at: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
});
export const user_role_relations = relations(user_roles, ({ one }) => ({

View file

@ -18,8 +18,8 @@ const users = pgTable('users', {
theme: text('theme').default('system'),
two_factor_secret: text('two_factor_secret').default(''),
two_factor_enabled: boolean('two_factor_enabled').default(false),
created_at: timestamp('created_at').notNull().defaultNow(),
updated_at: timestamp('updated_at').notNull().defaultNow(),
created_at: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
updated_at: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
});
export const user_relations = relations(users, ({ many }) => ({

View file

@ -15,8 +15,8 @@ const wishlist_items = pgTable('wishlist_items', {
game_id: uuid('game_id')
.notNull()
.references(() => games.id, { onDelete: 'cascade' }),
created_at: timestamp('created_at').notNull().defaultNow(),
updated_at: timestamp('updated_at').notNull().defaultNow(),
created_at: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
updated_at: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
});
export type WishlistItems = InferSelectModel<typeof wishlist_items>;

View file

@ -12,8 +12,8 @@ const wishlists = pgTable('wishlists', {
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
name: text('name').notNull().default('My Wishlist'),
created_at: timestamp('created_at').notNull().defaultNow(),
updated_at: timestamp('updated_at').notNull().defaultNow(),
created_at: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
updated_at: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
});
export type Wishlists = InferSelectModel<typeof wishlists>;

View file

@ -32,11 +32,10 @@ export const authentication: Handle = async function ({ event, resolve }) {
if (session && session.fresh) {
const sessionCookie = lucia.createSessionCookie(session.id);
console.log('sessionCookie', JSON.stringify(sessionCookie, null, 2));
// sveltekit types deviates from the de-facto standard
// you can use 'as any' too
// sveltekit types deviates from the de-facto standard, you can use 'as any' too
event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: '.',
...sessionCookie.attributes
...sessionCookie.attributes,
});
}
console.log('session from hooks', JSON.stringify(session, null, 2));
@ -45,7 +44,7 @@ export const authentication: Handle = async function ({ event, resolve }) {
console.log('blank sessionCookie', JSON.stringify(sessionCookie, null, 2));
event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: '.',
...sessionCookie.attributes
...sessionCookie.attributes,
});
}
event.locals.user = user;
@ -56,6 +55,6 @@ export const authentication: Handle = async function ({ event, resolve }) {
export const handle: Handle = sequence(
// Sentry.sentryHandle(),
authentication
authentication,
);
// export const handleError = Sentry.handleErrorWithSentry();

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = AvatarPrimitive.FallbackProps;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = AvatarPrimitive.ImageProps;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = AvatarPrimitive.Props;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { Button as ButtonPrimitive } from "bits-ui";
import { type Events, type Props, buttonVariants } from "./index.js";
import { cn } from "$lib/utils.js";
import { buttonVariants, type Props, type Events } from "./index.js";
type $$Props = Props;
type $$Events = Events;

View file

@ -1,9 +1,9 @@
import Root from "./button.svelte";
import { tv, type VariantProps } from "tailwind-variants";
import { type VariantProps, tv } from "tailwind-variants";
import type { Button as ButtonPrimitive } from "bits-ui";
import Root from "./button.svelte";
const buttonVariants = tv({
base: "inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
base: "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLParagraphElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils";
import type { HeadingLevel } from ".";
import type { HeadingLevel } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
tag?: HeadingLevel;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;

View file

@ -1,7 +1,8 @@
<script lang="ts">
import { Checkbox as CheckboxPrimitive } from "bits-ui";
import { Check, Minus } from "lucide-svelte";
import { cn } from "$lib/utils";
import Check from "lucide-svelte/icons/check";
import Minus from "lucide-svelte/icons/minus";
import { cn } from "$lib/utils.js";
type $$Props = CheckboxPrimitive.Props;
type $$Events = CheckboxPrimitive.Events;
@ -13,7 +14,7 @@
<CheckboxPrimitive.Root
class={cn(
"box-content peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50",
"peer box-content h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[disabled=true]:opacity-50",
className
)}
bind:checked
@ -21,7 +22,7 @@
on:click
>
<CheckboxPrimitive.Indicator
class={cn("flex items-center justify-center text-current h-4 w-4")}
class={cn("flex h-4 w-4 items-center justify-center text-current")}
let:isChecked
let:isIndeterminate
>

View file

@ -2,5 +2,5 @@ import Root from "./checkbox.svelte";
export {
Root,
//
Root as Checkbox
Root as Checkbox,
};

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.ContentProps;
type $$Events = DropdownMenuPrimitive.ContentEvents;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.ItemProps & {
inset?: boolean;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.LabelProps & {
inset?: boolean;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SeparatorProps;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SubContentProps;
type $$Events = DropdownMenuPrimitive.SubContentEvents;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;

View file

@ -1,9 +1,10 @@
<script lang="ts">
import * as Button from "$lib/components/ui/button";
import * as Button from "$lib/components/ui/button/index.js";
type $$Props = Button.Props;
type $$Events = Button.Events;
</script>
<Button.Root type="submit" {...$$restProps}>
<Button.Root type="submit" on:click on:keydown {...$$restProps}>
<slot />
</Button.Root>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined;

View file

@ -1,14 +1,13 @@
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPathLeaves, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = unknown;
type U = FormPathLeaves<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldErrorsProps & {
errorClasses?: string | undefined | null;

View file

@ -1,14 +1,13 @@
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = unknown;
type U = FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;

View file

@ -1,13 +1,12 @@
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = unknown;
type U = FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldsetProps<T, U>;

View file

@ -1,8 +1,8 @@
<script lang="ts">
import type { Label as LabelPrimitive } from "bits-ui";
import { getFormControl } from "formsnap";
import { cn } from "$lib/utils";
import { Label } from "$lib/components/ui/label";
import { cn } from "$lib/utils.js";
import { Label } from "$lib/components/ui/label/index.js";
type $$Props = LabelPrimitive.Props;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.LegendProps;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events;

View file

@ -30,5 +30,5 @@ export {
Value as SelectValue,
Content as SelectContent,
Trigger as SelectTrigger,
Separator as SelectSeparator
Separator as SelectSeparator,
};

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils";
import { scale } from "svelte/transition";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = SelectPrimitive.ContentProps;
type $$Events = SelectPrimitive.ContentEvents;
@ -13,7 +13,7 @@
export let outTransitionConfig: $$Props["outTransitionConfig"] = {
start: 0.95,
opacity: 0,
duration: 50
duration: 50,
};
let className: $$Props["class"] = undefined;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { cn } from "$lib/utils";
import Check from "lucide-svelte/icons/check";
import { Select as SelectPrimitive } from "bits-ui";
import { Check } from "lucide-svelte";
import { cn } from "$lib/utils.js";
type $$Props = SelectPrimitive.ItemProps;
type $$Events = SelectPrimitive.ItemEvents;
@ -18,7 +18,7 @@
{disabled}
{label}
class={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className
)}
{...$$restProps}
@ -34,5 +34,7 @@
<Check class="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<slot />
<slot>
{label || value}
</slot>
</SelectPrimitive.Item>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = SelectPrimitive.LabelProps;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = SelectPrimitive.SeparatorProps;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
import { ChevronDown } from "lucide-svelte";
import { cn } from "$lib/utils";
import ChevronDown from "lucide-svelte/icons/chevron-down";
import { cn } from "$lib/utils.js";
type $$Props = SelectPrimitive.TriggerProps;
type $$Events = SelectPrimitive.TriggerEvents;
@ -12,7 +12,7 @@
<SelectPrimitive.Trigger
class={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 line-clamp-1 truncate",
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...$$restProps}

View file

@ -3,5 +3,5 @@ import Root from "./switch.svelte";
export {
Root,
//
Root as Switch
Root as Switch,
};

View file

@ -1,8 +1,9 @@
<script lang="ts">
import { Switch as SwitchPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { cn } from "$lib/utils.js";
type $$Props = SwitchPrimitive.Props;
type $$Events = SwitchPrimitive.Events;
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = undefined;
@ -16,6 +17,8 @@
className
)}
{...$$restProps}
on:click
on:keydown
>
<SwitchPrimitive.Thumb
class={cn(

View file

@ -1,24 +1,24 @@
import { type VariantProps, tv } from "tailwind-variants";
import Root from "./toggle.svelte";
import { tv, type VariantProps } from "tailwind-variants";
export const toggleVariants = tv({
base: "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors data-[state=on]:bg-accent data-[state=on]:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background hover:bg-muted hover:text-muted-foreground",
base: "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
variants: {
variant: {
default: "bg-transparent",
outline:
"bg-transparent border border-input hover:bg-accent hover:text-accent-foreground"
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5"
}
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default"
}
size: "default",
},
});
export type Variant = VariantProps<typeof toggleVariants>["variant"];
@ -27,5 +27,5 @@ export type Size = VariantProps<typeof toggleVariants>["size"];
export {
Root,
//
Root as Toggle
Root as Toggle,
};

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { Toggle as TogglePrimitive } from "bits-ui";
import { toggleVariants, type Variant, type Size } from ".";
import { cn } from "$lib/utils";
import { type Size, type Variant, toggleVariants } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = TogglePrimitive.Props & {
variant?: Variant;

View file

@ -0,0 +1,15 @@
import { Tooltip as TooltipPrimitive } from "bits-ui";
import Content from "./tooltip-content.svelte";
const Root = TooltipPrimitive.Root;
const Trigger = TooltipPrimitive.Trigger;
export {
Root,
Trigger,
Content,
//
Root as Tooltip,
Content as TooltipContent,
Trigger as TooltipTrigger,
};

View file

@ -0,0 +1,28 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = TooltipPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export let sideOffset: $$Props["sideOffset"] = 4;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = {
y: 8,
duration: 150,
};
export { className as class };
</script>
<TooltipPrimitive.Content
{transition}
{transitionConfig}
{sideOffset}
class={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md",
className
)}
{...$$restProps}
>
<slot />
</TooltipPrimitive.Content>

View file

@ -20,6 +20,7 @@ export const lucia = new Lucia(adapter, {
return {
ipCountry: attributes.ip_country,
ipAddress: attributes.ip_address,
isTwoFactorAuthenticated: attributes.isTwoFactorAuthenticated,
};
},
getUserAttributes: (attributes) => {
@ -53,6 +54,7 @@ declare module 'lucia' {
interface DatabaseSessionAttributes {
ip_country: string;
ip_address: string;
isTwoFactorAuthenticated: boolean;
}
interface DatabaseUserAttributes {
username: string;

View file

@ -23,5 +23,8 @@ export const signInSchema = z.object({
.min(3, { message: 'Username must be at least 3 characters' })
.max(50, { message: 'Username must be less than 50 characters' }),
password: z.string({ required_error: 'Password is required' }).trim(),
totpToken: z.string().trim().min(6).max(10).optional(),
});
export const totpSchema = z.object({
totpToken: z.string().trim().min(6).max(10),
});

View file

@ -1,4 +1,5 @@
import { z } from 'zod';
import { IntegerString } from '$lib/zodValidation';
export type ListGame = {
id: string;
@ -13,7 +14,7 @@ export type ListGame = {
};
export const modifyListGameSchema = z.object({
id: z.string()
id: z.string(),
});
export type ModifyListGame = typeof modifyListGameSchema;
@ -21,24 +22,62 @@ export type ModifyListGame = typeof modifyListGameSchema;
export const userSchema = z.object({
firstName: z.string().trim().optional(),
lastName: z.string().trim().optional(),
email: z.string()
.trim()
.max(64, { message: 'Email must be less than 64 characters' })
.optional(),
email: z.string().trim().max(64, { message: 'Email must be less than 64 characters' }).optional(),
username: z
.string()
.trim()
.min(3, { message: 'Username must be at least 3 characters' })
.max(50, { message: 'Username must be less than 50 characters' }),
password: z
.string({ required_error: 'Password is required' })
.trim(),
confirm_password: z
.string({ required_error: 'Confirm Password is required' })
.trim(),
password: z.string({ required_error: 'Password is required' }).trim(),
confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(),
verified: z.boolean().default(false),
token: z.string().optional(),
receiveEmail: z.boolean().default(false),
createdAt: z.date().optional(),
updatedAt: z.date().optional()
updatedAt: z.date().optional(),
});
export const PaginationSchema = z.object({
skip: z.number().min(0).default(0),
limit: z.number().min(10).max(100).default(10),
});
export type PaginationSchema = typeof PaginationSchema;
export const FilterSchema = z.object({
status: z.string(),
author: z.string(),
});
export type FilterSchema = typeof FilterSchema;
export const SortSchema = z
.object({
sort: z.string(),
order: z.enum(['asc', 'desc']).default('asc'),
minAge: IntegerString(z.number().min(1).max(120).optional()),
minPlayers: IntegerString(z.number().min(1).max(50).optional()),
maxPlayers: IntegerString(z.number().min(1).max(50).optional()),
})
.superRefine(({ minPlayers, maxPlayers }, ctx) => {
console.log({ minPlayers, maxPlayers });
if (minPlayers && maxPlayers && minPlayers > maxPlayers) {
ctx.addIssue({
code: 'custom',
message: 'Min Players must be smaller than Max Players',
path: ['minPlayers'],
});
ctx.addIssue({
code: 'custom',
message: 'Min Players must be smaller than Max Players',
path: ['maxPlayers'],
});
}
});
export const SearchSchema = z.object({
q: z.string(),
exact: z.preprocess((a) => Boolean(a), z.boolean().default(true)),
});
export type SearchSchema = typeof SearchSchema;

View file

@ -5,7 +5,7 @@ export const BoardGameSearch = z.object({
minAge: z.number(),
maxAge: z.number(),
minPlayers: z.number(),
maxPlayers: z.number()
maxPlayers: z.number(),
});
export const saved_game_schema = z.object({
@ -13,26 +13,26 @@ export const saved_game_schema = z.object({
name: z.string(),
thumb_url: z.string(),
players: z.string(),
playtime: IntegerString(z.number())
playtime: IntegerString(z.number()),
});
export const list_game_request_schema = z.object({
id: z.string(),
externalId: z.string()
externalId: z.string(),
});
export type ListGameSchema = typeof list_game_request_schema;
// https://github.com/colinhacks/zod/discussions/330
function IntegerString<schema extends ZodNumber | ZodOptional<ZodNumber>>(schema: schema) {
export function IntegerString<schema extends ZodNumber | ZodOptional<ZodNumber>>(schema: schema) {
return z.preprocess(
(value) =>
typeof value === 'string'
? parseInt(value, 10)
: typeof value === 'number'
? value
: undefined,
schema
? value
: undefined,
schema,
);
}
@ -47,7 +47,7 @@ const Search = z.object({
sort: z.enum(['asc', 'desc']).optional(),
sortBy: z.enum(['name', 'min_players', 'max_players', 'min_age', 'times_played']).optional(),
limit: z.number().min(10).max(100).default(10),
skip: z.number().min(0).default(0)
skip: z.number().min(0).default(0),
});
// minAge: z
@ -71,7 +71,7 @@ export const search_schema = z
sort: z.enum(['asc', 'desc']).optional(),
order: z.enum(['name', 'min_players', 'max_players', 'min_age', 'times_played']).optional(),
limit: z.number().min(10).max(100).default(10),
skip: z.number().min(0).default(0)
skip: z.number().min(0).default(0),
})
.superRefine(
({ minPlayers, maxPlayers, minAge, exactMinAge, exactMinPlayers, exactMaxPlayers }, ctx) => {
@ -80,12 +80,12 @@ export const search_schema = z
ctx.addIssue({
code: 'custom',
message: 'Min Players must be smaller than Max Players',
path: ['minPlayers']
path: ['minPlayers'],
});
ctx.addIssue({
code: 'custom',
message: 'Min Players must be smaller than Max Players',
path: ['maxPlayers']
path: ['maxPlayers'],
});
}
@ -93,7 +93,7 @@ export const search_schema = z
ctx.addIssue({
code: 'custom',
message: 'Min Age required when searching for exact min age',
path: ['minAge']
path: ['minAge'],
});
}
@ -101,7 +101,7 @@ export const search_schema = z
ctx.addIssue({
code: 'custom',
message: 'Min Players required when searching for exact min players',
path: ['minPlayers']
path: ['minPlayers'],
});
}
@ -109,20 +109,20 @@ export const search_schema = z
ctx.addIssue({
code: 'custom',
message: 'Max Players required when searching for exact max players',
path: ['maxPlayers']
path: ['maxPlayers'],
});
}
}
},
);
export type SearchSchema = typeof search_schema;
export const BggForm = z.object({
link: z.string().trim().startsWith('https://boardgamegeek.com/boardgame/')
})
link: z.string().trim().startsWith('https://boardgamegeek.com/boardgame/'),
});
export const collection_search_schema = Search.extend({
collection_id: z.string()
collection_id: z.string(),
});
export type CollectionSearchSchema = typeof collection_search_schema;
@ -175,7 +175,7 @@ export const search_result_schema = z.object({
lt_reddit_count: z.number(),
lt_reddit_week_count: z.number(),
lt_reddit_day_count: z.number(),
fields: z.string()
fields: z.string(),
});
export type SearchResultSchema = typeof search_result_schema;
@ -199,18 +199,18 @@ export const game_schema = z.object({
min_age: z.number(),
description: z.string(),
players: z.string(),
playtime: z.string()
playtime: z.string(),
});
export const category_schema = z.object({
id: z.string(),
name: z.string()
name: z.string(),
});
export const mechanics_schema = z.object({
id: z.string(),
name: z.string(),
description: z.string().optional()
description: z.string().optional(),
});
const gameSchema = z.object({
@ -234,24 +234,24 @@ const gameSchema = z.object({
.array(
z.object({
id: z.string(),
name: z.string()
})
name: z.string(),
}),
)
.optional(),
publishers: z
.array(
z.object({
id: z.string(),
name: z.string()
})
name: z.string(),
}),
)
.optional(),
artists: z
.array(
z.object({
id: z.string(),
name: z.string()
})
name: z.string(),
}),
)
.optional(),
names: z.array(z.string()).optional(),
@ -260,26 +260,26 @@ const gameSchema = z.object({
z.object({
id: z.string(),
name: z.string(),
year_published: z.number().optional()
})
year_published: z.number().optional(),
}),
)
.optional(),
primary_publisher: z
.object({
id: z.string(),
name: z.string()
name: z.string(),
})
.optional()
.optional(),
});
const searchResultSchema = z.object({
games: z.array(gameSchema),
count: z.number()
count: z.number(),
});
export const WishlistSchema = z.object({
name: z.string(),
id: z.string()
id: z.string(),
});
// export const game_raw_schema_json = zodToJsonSchema(game_schema, {

View file

@ -59,15 +59,16 @@ export const actions: Actions = {
console.log('user', JSON.stringify(user, null, 2));
if (!user?.hashed_password) {
console.log('invalid username/password');
form.data.password = '';
return setError(form, '', 'Your username or password is incorrect.');
return setError(form, 'password', 'Your username or password is incorrect.');
}
const validPassword = await new Argon2id().verify(user.hashed_password, password);
if (!validPassword) {
console.log('invalid password');
form.data.password = '';
return setError(form, '', 'Your username or password is incorrect.');
return setError(form, 'password', 'Your username or password is incorrect.');
}
// await db
@ -101,11 +102,13 @@ export const actions: Actions = {
const usedRecoveryCode = await checkRecoveryCode(form?.data?.totpToken, user.id);
if (!usedRecoveryCode) {
console.log('invalid TOTP code');
form.errors.totpToken = ['Invalid code'];
return fail(400, {
form,
twoFactorRequired: true,
});
form.data.password = '';
// form.errors.totpToken = ['Invalid code'];
return setError(form, 'totpToken', 'Invalid code.');
// return fail(400, {
// form,
// twoFactorRequired: true,
// });
}
}
}
@ -114,6 +117,7 @@ export const actions: Actions = {
session = await lucia.createSession(user.id, {
ip_country: locals.country,
ip_address: locals.ip,
isTwoFactorAuthenticated: false,
});
console.log('logging in session', session);
sessionCookie = lucia.createSessionCookie(session.id);

View file

@ -0,0 +1,153 @@
import { fail, error, type Actions } from '@sveltejs/kit';
import { and, eq } from 'drizzle-orm';
import { Argon2id } from 'oslo/password';
import { decodeHex } from 'oslo/encoding';
import { TOTPController } from 'oslo/otp';
import { zod } from 'sveltekit-superforms/adapters';
import { setError, superValidate } from 'sveltekit-superforms/server';
import { redirect } from 'sveltekit-flash-message/server';
import { RateLimiter } from 'sveltekit-rate-limiter/server';
import db from '../../../db';
import { lucia } from '$lib/server/auth';
import { totpSchema } from '$lib/validations/auth';
import { users, recovery_codes } from '$db/schema';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
if (event.locals.user) {
const user = event.locals.user;
const dbUser = await db.query.users.findFirst({
where: eq(users.username, user.username),
});
const session = event.locals.session;
const isTwoFactorAuthenticated = session?.isTwoFactorAuthenticated;
if (isTwoFactorAuthenticated && dbUser?.two_factor_enabled && dbUser?.two_factor_secret) {
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
}
}
const form = await superValidate(event, zod(totpSchema));
return {
form,
};
};
const limiter = new RateLimiter({
// A rate is defined by [number, unit]
IPUA: [5, 'm'],
});
export const actions: Actions = {
default: async (event) => {
if (await limiter.isLimited(event)) {
throw error(429);
}
const { locals } = event;
const session = locals.session;
const user = locals.user;
if (!user || !session) {
throw fail(401);
}
const dbUser = await db.query.users.findFirst({
where: eq(users.username, user.username),
});
if (!dbUser) {
throw fail(401);
}
const isTwoFactorAuthenticated = session?.isTwoFactorAuthenticated;
if (isTwoFactorAuthenticated && dbUser?.two_factor_enabled && dbUser?.two_factor_secret) {
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
}
const form = await superValidate(event, zod(totpSchema));
if (!form.valid) {
form.data.totpToken = '';
return fail(400, {
form,
});
}
let sessionCookie;
try {
const totpToken = form?.data?.totpToken;
if (dbUser?.two_factor_enabled && dbUser?.two_factor_secret && !totpToken) {
return fail(400, {
form,
});
} else if (dbUser?.two_factor_enabled && dbUser?.two_factor_secret && totpToken) {
console.log('totpToken',totpToken);
const validOTP = await new TOTPController().verify(
form.data.totpToken,
decodeHex(dbUser.two_factor_secret),
);
console.log('validOTP', validOTP);
if (!validOTP) {
console.log('invalid TOTP code check for recovery codes');
const usedRecoveryCode = await checkRecoveryCode(totpToken, dbUser.id);
if (!usedRecoveryCode) {
console.log('invalid TOTP code');
return setError(form, 'totpToken', 'Invalid code.');
}
}
}
console.log('ip', locals.ip);
console.log('country', locals.country);
await lucia.invalidateSession(session.id);
const newSession = await lucia.createSession(dbUser.id, {
ip_country: locals.country,
ip_address: locals.ip,
isTwoFactorAuthenticated: true,
});
console.log('logging in session', newSession);
sessionCookie = lucia.createSessionCookie(newSession.id);
console.log('logging in session cookie', sessionCookie);
} catch (e) {
// TODO: need to return error message to the client
console.error(e);
return setError(form, 'totpToken', 'Error verifying TOTP code.');
}
console.log('setting session cookie', sessionCookie);
event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: '.',
...sessionCookie.attributes,
});
form.data.totpToken = '';
const message = { type: 'success', message: 'Signed In!' } as const;
redirect(302, '/', message, event);
},
};
async function checkRecoveryCode(recoveryCode: string, userId: string) {
const userRecoveryCodes = await db.query.recovery_codes.findMany({
where: and(eq(recovery_codes.used, false), eq(recovery_codes.userId, userId)),
});
for (const code of userRecoveryCodes) {
const validRecoveryCode = await new Argon2id().verify(code.code, recoveryCode);
if (validRecoveryCode) {
await db
.update(recovery_codes)
.set({
used: true,
})
.where(eq(recovery_codes.id, code.id));
return true;
}
}
return false;
}

View file

@ -0,0 +1,123 @@
<script lang="ts">
import { zodClient } from 'sveltekit-superforms/adapters';
import { superForm } from 'sveltekit-superforms/client';
import * as flashModule from 'sveltekit-flash-message/client';
import { AlertCircle } from "lucide-svelte";
import { signInSchema } from '$lib/validations/auth';
import * as Form from '$lib/components/ui/form';
import { Label } from '$components/ui/label';
import { Input } from '$components/ui/input';
import { Button } from '$components/ui/button';
import * as Alert from "$components/ui/alert";
import { boredState } from '$lib/stores/boredState.js';
export let data;
export let form;
const superLoginForm = superForm(data.form, {
onSubmit: () => boredState.update((n) => ({ ...n, loading: true })),
onResult: () => boredState.update((n) => ({ ...n, loading: false })),
flashMessage: {
module: flashModule,
onError: ({ result, message }) => {
// Error handling for the flash message:
// - result is the ActionResult
// - message is the flash store (not the status message store)
const errorMessage = result.error.message
message.set({ type: 'error', message: errorMessage });
}
},
syncFlashMessage: false,
taintedMessage: null,
validators: zodClient(signInSchema),
validationMethod: 'oninput',
delayMs: 0,
});
const { form: loginForm, errors, enhance } = superLoginForm;
</script>
<svelte:head>
<title>Bored Game | Login</title>
</svelte:head>
<div class="login">
<form method="POST" use:enhance>
<h2
class="scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0"
>
Please enter your two factor code
</h2>
<Form.Field form={superLoginForm} name="username">
<Form.Control let:attrs>
<Form.Label for="username">Username</Form.Label>
<Input {...attrs} autocomplete="username" bind:value={$loginForm.username} />
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field form={superLoginForm} name="password">
<Form.Control let:attrs>
<Form.Label for="password">Password</Form.Label>
<Input {...attrs} autocomplete="current-password" type="password" bind:value={$loginForm.password} />
</Form.Control>
<Form.FieldErrors />
</Form.Field>
{#if form?.twoFactorRequired}
<Form.Field form={superLoginForm} name="totpToken">
<Form.Control let:attrs>
<Form.Label for="totpToken">Two Factor Code or Recovery Code</Form.Label>
<Input {...attrs} autocomplete="one-time-code" bind:value={$loginForm.totpToken} />
</Form.Control>
<Form.FieldErrors />
</Form.Field>
{/if}
<Form.Button>Login</Form.Button>
<p class="px-8 text-center text-sm text-muted-foreground">
By clicking continue, you agree to our
<a href="/terms" class="underline underline-offset-4 hover:text-primary">
Terms of Use
</a>
and
<a href="/privacy" class="underline underline-offset-4 hover:text-primary">
Privacy Policy
</a>.
</p>
</form>
</div>
<style lang="postcss">
.loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 101;
display: grid;
place-items: center;
gap: 1rem;
h3 {
color: white;
}
}
.login {
display: flex;
margin-top: 1.5rem;
flex-direction: column;
justify-content: center;
width: 100%;
margin-right: auto;
margin-left: auto;
@media (min-width: 640px) {
width: 350px;
}
form {
display: grid;
gap: 0.5rem;
align-items: center;
max-width: 24rem;
}
}
</style>

View file

@ -3,10 +3,28 @@ import db from '../../../../db';
import { asc, desc, eq, ilike, or } from 'drizzle-orm';
import { games } from '$db/schema';
import kebabCase from 'just-kebab-case';
import {
FilterSchema,
PaginationSchema,
SearchSchema,
SortSchema,
} from '$lib/validations/zod-schemas';
// Search a user's collection
export const GET = async ({ url, locals }) => {
const searchParams = Object.fromEntries(url.searchParams);
const searchGames = PaginationSchema.merge(FilterSchema)
.merge(SortSchema)
.merge(SearchSchema)
.parse(searchParams);
if (searchGames.status !== 'success') {
error(400, 'Invalid request');
}
const { q, limit, skip, order, exact } = searchGames;
const q = searchParams?.q?.trim() || '';
const limit = parseInt(searchParams?.limit) || 10;
const skip = parseInt(searchParams?.skip) || 0;

View file

@ -1,27 +0,0 @@
import { customType } from 'drizzle-orm/pg-core';
import {sql} from "drizzle-orm";
function genExpWithWeights(input: string[]) {
const columnExpressions = input.map((column, index) => {
const weight = String.fromCharCode(index + 65);
return sql`setweight(to_tsvector('english', coalesce(${column}, '')), '${weight}')`;
});
return sql`tsvector GENERATED ALWAYS AS (${columnExpressions.join(' || ')}) STORED`;
}
export const tsvector = customType<{
data: string;
config: { sources: string[]; weighted: boolean };
}>({
dataType(config) {
if (config) {
const sources = config.sources.join(" || ' ' || ");
return config.weighted
? genExpWithWeights(config.sources)
: sql`tsvector generated always as (to_tsvector('english', ${sources})) stored`;
} else {
return sql`tsvector`;
}
}
});