From fc5c16e6dd53a0c1725ee1081370888bbe8ebb97 Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Thu, 29 May 2025 20:36:42 +0300 Subject: [PATCH] feat: privacy policy + download data & delete account --- .../drizzle/0012_glamorous_white_tiger.sql | 70 + website/drizzle/0013_big_champions.sql | 1 + website/drizzle/meta/0012_snapshot.json | 1503 ++++++++++++++++ website/drizzle/meta/0013_snapshot.json | 1519 +++++++++++++++++ website/drizzle/meta/_journal.json | 14 + website/src/hooks.server.ts | 4 +- website/src/lib/server/db/schema.ts | 31 +- website/src/lib/server/job.ts | 74 +- website/src/lib/types/prediction.ts | 13 +- .../api/settings/data-download/+server.ts | 240 +++ .../api/settings/delete-account/+server.ts | 58 + website/src/routes/hopium/[id]/+page.svelte | 75 +- website/src/routes/legal/privacy/+page.svelte | 413 +++++ website/src/routes/settings/+page.svelte | 196 ++- 14 files changed, 4159 insertions(+), 52 deletions(-) create mode 100644 website/drizzle/0012_glamorous_white_tiger.sql create mode 100644 website/drizzle/0013_big_champions.sql create mode 100644 website/drizzle/meta/0012_snapshot.json create mode 100644 website/drizzle/meta/0013_snapshot.json create mode 100644 website/src/routes/api/settings/data-download/+server.ts create mode 100644 website/src/routes/api/settings/delete-account/+server.ts create mode 100644 website/src/routes/legal/privacy/+page.svelte diff --git a/website/drizzle/0012_glamorous_white_tiger.sql b/website/drizzle/0012_glamorous_white_tiger.sql new file mode 100644 index 0000000..9f8b8b9 --- /dev/null +++ b/website/drizzle/0012_glamorous_white_tiger.sql @@ -0,0 +1,70 @@ +CREATE TABLE IF NOT EXISTS "account_deletion_request" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "requested_at" timestamp with time zone DEFAULT now() NOT NULL, + "scheduled_deletion_at" timestamp with time zone NOT NULL, + "reason" text, + "is_processed" boolean DEFAULT false NOT NULL, + CONSTRAINT "account_deletion_request_user_id_unique" UNIQUE("user_id") +); +--> statement-breakpoint +ALTER TABLE "comment" DROP CONSTRAINT "comment_user_id_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "prediction_bet" DROP CONSTRAINT "prediction_bet_user_id_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "prediction_question" DROP CONSTRAINT "prediction_question_creator_id_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "promo_code" DROP CONSTRAINT "promo_code_created_by_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "promo_code_redemption" DROP CONSTRAINT "promo_code_redemption_user_id_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "transaction" DROP CONSTRAINT "transaction_user_id_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "comment" ALTER COLUMN "user_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "prediction_bet" ALTER COLUMN "user_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "prediction_question" ALTER COLUMN "creator_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "promo_code_redemption" ALTER COLUMN "user_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "transaction" ALTER COLUMN "user_id" DROP NOT NULL;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "account_deletion_request" ADD CONSTRAINT "account_deletion_request_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "account_deletion_request_user_id_idx" ON "account_deletion_request" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "account_deletion_request_scheduled_deletion_idx" ON "account_deletion_request" USING btree ("scheduled_deletion_at");--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "comment" ADD CONSTRAINT "comment_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "prediction_bet" ADD CONSTRAINT "prediction_bet_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "prediction_question" ADD CONSTRAINT "prediction_question_creator_id_user_id_fk" FOREIGN KEY ("creator_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "promo_code" ADD CONSTRAINT "promo_code_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "promo_code_redemption" ADD CONSTRAINT "promo_code_redemption_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "transaction" ADD CONSTRAINT "transaction_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/website/drizzle/0013_big_champions.sql b/website/drizzle/0013_big_champions.sql new file mode 100644 index 0000000..4ad43ac --- /dev/null +++ b/website/drizzle/0013_big_champions.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS "account_deletion_request_open_idx" ON "account_deletion_request" USING btree ("user_id") WHERE is_processed = false; \ No newline at end of file diff --git a/website/drizzle/meta/0012_snapshot.json b/website/drizzle/meta/0012_snapshot.json new file mode 100644 index 0000000..810960b --- /dev/null +++ b/website/drizzle/meta/0012_snapshot.json @@ -0,0 +1,1503 @@ +{ + "id": "51266d99-8120-41ae-9c8a-45945c6d442a", + "prevId": "de0863ba-af00-4bfe-a767-cc1a91275b10", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "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()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.account_deletion_request": { + "name": "account_deletion_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "scheduled_deletion_at": { + "name": "scheduled_deletion_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_processed": { + "name": "is_processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "account_deletion_request_user_id_idx": { + "name": "account_deletion_request_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_deletion_request_scheduled_deletion_idx": { + "name": "account_deletion_request_scheduled_deletion_idx", + "columns": [ + { + "expression": "scheduled_deletion_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_deletion_request_user_id_user_id_fk": { + "name": "account_deletion_request_user_id_user_id_fk", + "tableFrom": "account_deletion_request", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "account_deletion_request_user_id_unique": { + "name": "account_deletion_request_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + } + }, + "public.coin": { + "name": "coin", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "symbol": { + "name": "symbol", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "initial_supply": { + "name": "initial_supply", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true + }, + "circulating_supply": { + "name": "circulating_supply", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true + }, + "current_price": { + "name": "current_price", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "market_cap": { + "name": "market_cap", + "type": "numeric(30, 2)", + "primaryKey": false, + "notNull": true + }, + "volume_24h": { + "name": "volume_24h", + "type": "numeric(30, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0.00'" + }, + "change_24h": { + "name": "change_24h", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.0000'" + }, + "pool_coin_amount": { + "name": "pool_coin_amount", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true, + "default": "'0.00000000'" + }, + "pool_base_currency_amount": { + "name": "pool_base_currency_amount", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true, + "default": "'0.00000000'" + }, + "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()" + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "coin_creator_id_user_id_fk": { + "name": "coin_creator_id_user_id_fk", + "tableFrom": "coin", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "coin_symbol_unique": { + "name": "coin_symbol_unique", + "nullsNotDistinct": false, + "columns": [ + "symbol" + ] + } + } + }, + "public.comment": { + "name": "comment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "coin_id": { + "name": "coin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "likes_count": { + "name": "likes_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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()" + }, + "is_deleted": { + "name": "is_deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "comment_user_id_idx": { + "name": "comment_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comment_coin_id_idx": { + "name": "comment_coin_id_idx", + "columns": [ + { + "expression": "coin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "comment_user_id_user_id_fk": { + "name": "comment_user_id_user_id_fk", + "tableFrom": "comment", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "comment_coin_id_coin_id_fk": { + "name": "comment_coin_id_coin_id_fk", + "tableFrom": "comment", + "tableTo": "coin", + "columnsFrom": [ + "coin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.comment_like": { + "name": "comment_like", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment_id": { + "name": "comment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comment_like_user_id_user_id_fk": { + "name": "comment_like_user_id_user_id_fk", + "tableFrom": "comment_like", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comment_like_comment_id_comment_id_fk": { + "name": "comment_like_comment_id_comment_id_fk", + "tableFrom": "comment_like", + "tableTo": "comment", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "comment_like_user_id_comment_id_pk": { + "name": "comment_like_user_id_comment_id_pk", + "columns": [ + "user_id", + "comment_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.prediction_bet": { + "name": "prediction_bet", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "question_id": { + "name": "question_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "side": { + "name": "side", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "actual_winnings": { + "name": "actual_winnings", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "prediction_bet_user_id_idx": { + "name": "prediction_bet_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_bet_question_id_idx": { + "name": "prediction_bet_question_id_idx", + "columns": [ + { + "expression": "question_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_bet_user_question_idx": { + "name": "prediction_bet_user_question_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "question_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_bet_created_at_idx": { + "name": "prediction_bet_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "prediction_bet_user_id_user_id_fk": { + "name": "prediction_bet_user_id_user_id_fk", + "tableFrom": "prediction_bet", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "prediction_bet_question_id_prediction_question_id_fk": { + "name": "prediction_bet_question_id_prediction_question_id_fk", + "tableFrom": "prediction_bet", + "tableTo": "prediction_question", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.prediction_question": { + "name": "prediction_question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "creator_id": { + "name": "creator_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "question": { + "name": "question", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "prediction_market_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "resolution_date": { + "name": "resolution_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ai_resolution": { + "name": "ai_resolution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "total_yes_amount": { + "name": "total_yes_amount", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true, + "default": "'0.00000000'" + }, + "total_no_amount": { + "name": "total_no_amount", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true, + "default": "'0.00000000'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "requires_web_search": { + "name": "requires_web_search", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "validation_reason": { + "name": "validation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "prediction_question_creator_id_idx": { + "name": "prediction_question_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_question_status_idx": { + "name": "prediction_question_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_question_resolution_date_idx": { + "name": "prediction_question_resolution_date_idx", + "columns": [ + { + "expression": "resolution_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "prediction_question_creator_id_user_id_fk": { + "name": "prediction_question_creator_id_user_id_fk", + "tableFrom": "prediction_question", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.price_history": { + "name": "price_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "coin_id": { + "name": "coin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "price_history_coin_id_coin_id_fk": { + "name": "price_history_coin_id_coin_id_fk", + "tableFrom": "price_history", + "tableTo": "coin", + "columnsFrom": [ + "coin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.promo_code": { + "name": "promo_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_amount": { + "name": "reward_amount", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "expires_at": { + "name": "expires_at", + "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()" + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "promo_code_created_by_user_id_fk": { + "name": "promo_code_created_by_user_id_fk", + "tableFrom": "promo_code", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "promo_code_code_unique": { + "name": "promo_code_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + } + }, + "public.promo_code_redemption": { + "name": "promo_code_redemption", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "promo_code_id": { + "name": "promo_code_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reward_amount": { + "name": "reward_amount", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "promo_code_redemption_user_id_user_id_fk": { + "name": "promo_code_redemption_user_id_user_id_fk", + "tableFrom": "promo_code_redemption", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "promo_code_redemption_promo_code_id_promo_code_id_fk": { + "name": "promo_code_redemption_promo_code_id_promo_code_id_fk", + "tableFrom": "promo_code_redemption", + "tableTo": "promo_code", + "columnsFrom": [ + "promo_code_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "promo_code_redemption_user_id_promo_code_id_unique": { + "name": "promo_code_redemption_user_id_promo_code_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "promo_code_id" + ] + } + } + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "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()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + } + }, + "public.transaction": { + "name": "transaction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "coin_id": { + "name": "coin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "transaction_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true + }, + "price_per_coin": { + "name": "price_per_coin", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "total_base_currency_amount": { + "name": "total_base_currency_amount", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "transaction_user_id_user_id_fk": { + "name": "transaction_user_id_user_id_fk", + "tableFrom": "transaction", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "transaction_coin_id_coin_id_fk": { + "name": "transaction_coin_id_coin_id_fk", + "tableFrom": "transaction", + "tableTo": "coin", + "columnsFrom": [ + "coin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "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()" + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_banned": { + "name": "is_banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_currency_balance": { + "name": "base_currency_balance", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true, + "default": "'10000.00000000'" + }, + "bio": { + "name": "bio", + "type": "varchar(160)", + "primaryKey": false, + "notNull": false, + "default": "'Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”'" + }, + "username": { + "name": "username", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "volume_master": { + "name": "volume_master", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0.70'" + }, + "volume_muted": { + "name": "volume_muted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_reward_claim": { + "name": "last_reward_claim", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_rewards_claimed": { + "name": "total_rewards_claimed", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true, + "default": "'0.00000000'" + }, + "login_streak": { + "name": "login_streak", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + } + }, + "public.user_portfolio": { + "name": "user_portfolio", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "coin_id": { + "name": "coin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_portfolio_user_id_user_id_fk": { + "name": "user_portfolio_user_id_user_id_fk", + "tableFrom": "user_portfolio", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_portfolio_coin_id_coin_id_fk": { + "name": "user_portfolio_coin_id_coin_id_fk", + "tableFrom": "user_portfolio", + "tableTo": "coin", + "columnsFrom": [ + "coin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_portfolio_user_id_coin_id_pk": { + "name": "user_portfolio_user_id_coin_id_pk", + "columns": [ + "user_id", + "coin_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "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": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "public.prediction_market_status": { + "name": "prediction_market_status", + "schema": "public", + "values": [ + "ACTIVE", + "RESOLVED", + "CANCELLED" + ] + }, + "public.transaction_type": { + "name": "transaction_type", + "schema": "public", + "values": [ + "BUY", + "SELL" + ] + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/website/drizzle/meta/0013_snapshot.json b/website/drizzle/meta/0013_snapshot.json new file mode 100644 index 0000000..58aaf70 --- /dev/null +++ b/website/drizzle/meta/0013_snapshot.json @@ -0,0 +1,1519 @@ +{ + "id": "91593e7f-6e33-47ad-83f3-945d6ee19453", + "prevId": "51266d99-8120-41ae-9c8a-45945c6d442a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "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()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.account_deletion_request": { + "name": "account_deletion_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "scheduled_deletion_at": { + "name": "scheduled_deletion_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_processed": { + "name": "is_processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "account_deletion_request_user_id_idx": { + "name": "account_deletion_request_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_deletion_request_scheduled_deletion_idx": { + "name": "account_deletion_request_scheduled_deletion_idx", + "columns": [ + { + "expression": "scheduled_deletion_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_deletion_request_open_idx": { + "name": "account_deletion_request_open_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "is_processed = false", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_deletion_request_user_id_user_id_fk": { + "name": "account_deletion_request_user_id_user_id_fk", + "tableFrom": "account_deletion_request", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "account_deletion_request_user_id_unique": { + "name": "account_deletion_request_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + } + }, + "public.coin": { + "name": "coin", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "symbol": { + "name": "symbol", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "initial_supply": { + "name": "initial_supply", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true + }, + "circulating_supply": { + "name": "circulating_supply", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true + }, + "current_price": { + "name": "current_price", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "market_cap": { + "name": "market_cap", + "type": "numeric(30, 2)", + "primaryKey": false, + "notNull": true + }, + "volume_24h": { + "name": "volume_24h", + "type": "numeric(30, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0.00'" + }, + "change_24h": { + "name": "change_24h", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.0000'" + }, + "pool_coin_amount": { + "name": "pool_coin_amount", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true, + "default": "'0.00000000'" + }, + "pool_base_currency_amount": { + "name": "pool_base_currency_amount", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true, + "default": "'0.00000000'" + }, + "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()" + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "coin_creator_id_user_id_fk": { + "name": "coin_creator_id_user_id_fk", + "tableFrom": "coin", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "coin_symbol_unique": { + "name": "coin_symbol_unique", + "nullsNotDistinct": false, + "columns": [ + "symbol" + ] + } + } + }, + "public.comment": { + "name": "comment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "coin_id": { + "name": "coin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "likes_count": { + "name": "likes_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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()" + }, + "is_deleted": { + "name": "is_deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "comment_user_id_idx": { + "name": "comment_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comment_coin_id_idx": { + "name": "comment_coin_id_idx", + "columns": [ + { + "expression": "coin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "comment_user_id_user_id_fk": { + "name": "comment_user_id_user_id_fk", + "tableFrom": "comment", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "comment_coin_id_coin_id_fk": { + "name": "comment_coin_id_coin_id_fk", + "tableFrom": "comment", + "tableTo": "coin", + "columnsFrom": [ + "coin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.comment_like": { + "name": "comment_like", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment_id": { + "name": "comment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comment_like_user_id_user_id_fk": { + "name": "comment_like_user_id_user_id_fk", + "tableFrom": "comment_like", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comment_like_comment_id_comment_id_fk": { + "name": "comment_like_comment_id_comment_id_fk", + "tableFrom": "comment_like", + "tableTo": "comment", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "comment_like_user_id_comment_id_pk": { + "name": "comment_like_user_id_comment_id_pk", + "columns": [ + "user_id", + "comment_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.prediction_bet": { + "name": "prediction_bet", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "question_id": { + "name": "question_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "side": { + "name": "side", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "actual_winnings": { + "name": "actual_winnings", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "prediction_bet_user_id_idx": { + "name": "prediction_bet_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_bet_question_id_idx": { + "name": "prediction_bet_question_id_idx", + "columns": [ + { + "expression": "question_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_bet_user_question_idx": { + "name": "prediction_bet_user_question_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "question_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_bet_created_at_idx": { + "name": "prediction_bet_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "prediction_bet_user_id_user_id_fk": { + "name": "prediction_bet_user_id_user_id_fk", + "tableFrom": "prediction_bet", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "prediction_bet_question_id_prediction_question_id_fk": { + "name": "prediction_bet_question_id_prediction_question_id_fk", + "tableFrom": "prediction_bet", + "tableTo": "prediction_question", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.prediction_question": { + "name": "prediction_question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "creator_id": { + "name": "creator_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "question": { + "name": "question", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "prediction_market_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "resolution_date": { + "name": "resolution_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ai_resolution": { + "name": "ai_resolution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "total_yes_amount": { + "name": "total_yes_amount", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true, + "default": "'0.00000000'" + }, + "total_no_amount": { + "name": "total_no_amount", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true, + "default": "'0.00000000'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "requires_web_search": { + "name": "requires_web_search", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "validation_reason": { + "name": "validation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "prediction_question_creator_id_idx": { + "name": "prediction_question_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_question_status_idx": { + "name": "prediction_question_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_question_resolution_date_idx": { + "name": "prediction_question_resolution_date_idx", + "columns": [ + { + "expression": "resolution_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "prediction_question_creator_id_user_id_fk": { + "name": "prediction_question_creator_id_user_id_fk", + "tableFrom": "prediction_question", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.price_history": { + "name": "price_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "coin_id": { + "name": "coin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "price_history_coin_id_coin_id_fk": { + "name": "price_history_coin_id_coin_id_fk", + "tableFrom": "price_history", + "tableTo": "coin", + "columnsFrom": [ + "coin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.promo_code": { + "name": "promo_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_amount": { + "name": "reward_amount", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "expires_at": { + "name": "expires_at", + "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()" + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "promo_code_created_by_user_id_fk": { + "name": "promo_code_created_by_user_id_fk", + "tableFrom": "promo_code", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "promo_code_code_unique": { + "name": "promo_code_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + } + }, + "public.promo_code_redemption": { + "name": "promo_code_redemption", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "promo_code_id": { + "name": "promo_code_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reward_amount": { + "name": "reward_amount", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "promo_code_redemption_user_id_user_id_fk": { + "name": "promo_code_redemption_user_id_user_id_fk", + "tableFrom": "promo_code_redemption", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "promo_code_redemption_promo_code_id_promo_code_id_fk": { + "name": "promo_code_redemption_promo_code_id_promo_code_id_fk", + "tableFrom": "promo_code_redemption", + "tableTo": "promo_code", + "columnsFrom": [ + "promo_code_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "promo_code_redemption_user_id_promo_code_id_unique": { + "name": "promo_code_redemption_user_id_promo_code_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "promo_code_id" + ] + } + } + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "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()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + } + }, + "public.transaction": { + "name": "transaction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "coin_id": { + "name": "coin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "transaction_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true + }, + "price_per_coin": { + "name": "price_per_coin", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "total_base_currency_amount": { + "name": "total_base_currency_amount", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "transaction_user_id_user_id_fk": { + "name": "transaction_user_id_user_id_fk", + "tableFrom": "transaction", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "transaction_coin_id_coin_id_fk": { + "name": "transaction_coin_id_coin_id_fk", + "tableFrom": "transaction", + "tableTo": "coin", + "columnsFrom": [ + "coin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "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()" + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_banned": { + "name": "is_banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_currency_balance": { + "name": "base_currency_balance", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true, + "default": "'10000.00000000'" + }, + "bio": { + "name": "bio", + "type": "varchar(160)", + "primaryKey": false, + "notNull": false, + "default": "'Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”'" + }, + "username": { + "name": "username", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "volume_master": { + "name": "volume_master", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0.70'" + }, + "volume_muted": { + "name": "volume_muted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_reward_claim": { + "name": "last_reward_claim", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_rewards_claimed": { + "name": "total_rewards_claimed", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true, + "default": "'0.00000000'" + }, + "login_streak": { + "name": "login_streak", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + } + }, + "public.user_portfolio": { + "name": "user_portfolio", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "coin_id": { + "name": "coin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_portfolio_user_id_user_id_fk": { + "name": "user_portfolio_user_id_user_id_fk", + "tableFrom": "user_portfolio", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_portfolio_coin_id_coin_id_fk": { + "name": "user_portfolio_coin_id_coin_id_fk", + "tableFrom": "user_portfolio", + "tableTo": "coin", + "columnsFrom": [ + "coin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_portfolio_user_id_coin_id_pk": { + "name": "user_portfolio_user_id_coin_id_pk", + "columns": [ + "user_id", + "coin_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "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": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "public.prediction_market_status": { + "name": "prediction_market_status", + "schema": "public", + "values": [ + "ACTIVE", + "RESOLVED", + "CANCELLED" + ] + }, + "public.transaction_type": { + "name": "transaction_type", + "schema": "public", + "values": [ + "BUY", + "SELL" + ] + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/website/drizzle/meta/_journal.json b/website/drizzle/meta/_journal.json index 18d9623..d92c193 100644 --- a/website/drizzle/meta/_journal.json +++ b/website/drizzle/meta/_journal.json @@ -85,6 +85,20 @@ "when": 1748528211995, "tag": "0011_broken_risque", "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1748537205986, + "tag": "0012_glamorous_white_tiger", + "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1748540176485, + "tag": "0013_big_champions", + "breakpoints": true } ] } \ No newline at end of file diff --git a/website/src/hooks.server.ts b/website/src/hooks.server.ts index d2a7a2f..118e082 100644 --- a/website/src/hooks.server.ts +++ b/website/src/hooks.server.ts @@ -1,5 +1,5 @@ import { auth } from "$lib/auth"; -import { resolveExpiredQuestions } from "$lib/server/job"; +import { resolveExpiredQuestions, processAccountDeletions } from "$lib/server/job"; import { svelteKitHandler } from "better-auth/svelte-kit"; import { redis } from "$lib/server/redis"; import { building } from '$app/environment'; @@ -38,9 +38,11 @@ async function initializeScheduler() { }, (lockTTL / 2) * 1000); // Renew at half the TTL resolveExpiredQuestions().catch(console.error); + processAccountDeletions().catch(console.error); const schedulerInterval = setInterval(() => { resolveExpiredQuestions().catch(console.error); + processAccountDeletions().catch(console.error); }, 5 * 60 * 1000); // Cleanup on process exit diff --git a/website/src/lib/server/db/schema.ts b/website/src/lib/server/db/schema.ts index 7242036..122c54c 100644 --- a/website/src/lib/server/db/schema.ts +++ b/website/src/lib/server/db/schema.ts @@ -21,7 +21,7 @@ export const user = pgTable("user", { }).notNull().default("10000.00000000"), // 10,000 *BUSS bio: varchar("bio", { length: 160 }).default("Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”"), username: varchar("username", { length: 30 }).notNull().unique(), - + volumeMaster: decimal("volume_master", { precision: 3, scale: 2 }).notNull().default("0.70"), volumeMuted: boolean("volume_muted").notNull().default(false), @@ -103,7 +103,7 @@ export const userPortfolio = pgTable("user_portfolio", { export const transaction = pgTable("transaction", { id: serial("id").primaryKey(), - userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }), + userId: integer("user_id").references(() => user.id, { onDelete: "set null" }), coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }), type: transactionTypeEnum("type").notNull(), quantity: decimal("quantity", { precision: 30, scale: 8 }).notNull(), @@ -121,7 +121,7 @@ export const priceHistory = pgTable("price_history", { export const comment = pgTable("comment", { id: serial("id").primaryKey(), - userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }), + userId: integer("user_id").references(() => user.id, { onDelete: "set null" }), coinId: integer("coin_id").notNull().references(() => coin.id, { onDelete: "cascade" }), content: varchar("content", { length: 500 }).notNull(), likesCount: integer("likes_count").notNull().default(0), @@ -154,12 +154,12 @@ export const promoCode = pgTable('promo_code', { isActive: boolean('is_active').notNull().default(true), expiresAt: timestamp('expires_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), - createdBy: integer('created_by').references(() => user.id), + createdBy: integer('created_by').references(() => user.id, { onDelete: "set null" }), }); export const promoCodeRedemption = pgTable('promo_code_redemption', { id: serial('id').primaryKey(), - userId: integer('user_id').notNull().references(() => user.id), + userId: integer('user_id').references(() => user.id, { onDelete: "cascade" }), promoCodeId: integer('promo_code_id').notNull().references(() => promoCode.id), rewardAmount: decimal('reward_amount', { precision: 20, scale: 8 }).notNull(), redeemedAt: timestamp('redeemed_at', { withTimezone: true }).notNull().defaultNow(), @@ -169,7 +169,7 @@ export const promoCodeRedemption = pgTable('promo_code_redemption', { export const predictionQuestion = pgTable("prediction_question", { id: serial("id").primaryKey(), - creatorId: integer("creator_id").notNull().references(() => user.id, { onDelete: "cascade" }), + creatorId: integer("creator_id").references(() => user.id, { onDelete: "set null" }), question: varchar("question", { length: 200 }).notNull(), status: predictionMarketEnum("status").notNull().default("ACTIVE"), resolutionDate: timestamp("resolution_date", { withTimezone: true }).notNull(), @@ -192,7 +192,7 @@ export const predictionQuestion = pgTable("prediction_question", { export const predictionBet = pgTable("prediction_bet", { id: serial("id").primaryKey(), - userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }), + userId: integer("user_id").references(() => user.id, { onDelete: "set null" }), questionId: integer("question_id").notNull().references(() => predictionQuestion.id, { onDelete: "cascade" }), side: boolean("side").notNull(), // true = YES, false = NO amount: decimal("amount", { precision: 20, scale: 8 }).notNull(), @@ -207,4 +207,21 @@ export const predictionBet = pgTable("prediction_bet", { createdAtIdx: index("prediction_bet_created_at_idx").on(table.createdAt), amountCheck: check("amount_positive", sql`amount > 0`), }; +}); + +export const accountDeletionRequest = pgTable("account_deletion_request", { + id: serial("id").primaryKey(), + userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }).unique(), + requestedAt: timestamp("requested_at", { withTimezone: true }).notNull().defaultNow(), + scheduledDeletionAt: timestamp("scheduled_deletion_at", { withTimezone: true }).notNull(), + reason: text("reason"), + isProcessed: boolean("is_processed").notNull().default(false), +}, (table) => { + return { + userIdIdx: index("account_deletion_request_user_id_idx").on(table.userId), + scheduledDeletionIdx: index("account_deletion_request_scheduled_deletion_idx").on(table.scheduledDeletionAt), + oneOpenRequest: index("account_deletion_request_open_idx") + .on(table.userId) + .where(sql`is_processed = false`), + }; }); \ No newline at end of file diff --git a/website/src/lib/server/job.ts b/website/src/lib/server/job.ts index 8673ce4..d12e0d7 100644 --- a/website/src/lib/server/job.ts +++ b/website/src/lib/server/job.ts @@ -1,5 +1,5 @@ import { db } from '$lib/server/db'; -import { predictionQuestion, predictionBet, user } from '$lib/server/db/schema'; +import { predictionQuestion, predictionBet, user, accountDeletionRequest, session, account, promoCodeRedemption, userPortfolio, commentLike, comment, transaction, coin } from '$lib/server/db/schema'; import { eq, and, lte, isNull } from 'drizzle-orm'; import { resolveQuestion, getRugplayData } from '$lib/server/ai'; @@ -83,7 +83,7 @@ export async function resolveExpiredQuestions() { }) .where(eq(predictionBet.id, bet.id)); - if (won && winnings > 0) { + if (won && winnings > 0 && bet.userId !== null) { const [userData] = await tx .select({ baseCurrencyBalance: user.baseCurrencyBalance }) .from(user) @@ -113,4 +113,74 @@ export async function resolveExpiredQuestions() { } catch (error) { console.error('Error in resolveExpiredQuestions:', error); } +} + +export async function processAccountDeletions() { + const now = new Date(); + + try { + const expiredRequests = await db.select() + .from(accountDeletionRequest) + .where( + and( + lte(accountDeletionRequest.scheduledDeletionAt, now), + eq(accountDeletionRequest.isProcessed, false) + ) + ); + + console.log(`🗑️ Processing ${expiredRequests.length} expired account deletion requests`); + + for (const request of expiredRequests) { + try { + await db.transaction(async (tx) => { + const userId = request.userId; + + await tx.update(transaction) + .set({ userId: null }) + .where(eq(transaction.userId, userId)); + + await tx.update(comment) + .set({ userId: null, content: "[deleted]", isDeleted: true }) + .where(eq(comment.userId, userId)); + + await tx.update(predictionBet) + .set({ userId: null }) + .where(eq(predictionBet.userId, userId)); + + await tx.update(predictionQuestion) + .set({ creatorId: null }) + .where(eq(predictionQuestion.creatorId, userId)); + + await tx.update(coin) + .set({ creatorId: null }) + .where(eq(coin.creatorId, userId)); + + await tx.delete(session).where(eq(session.userId, userId)); + await tx.delete(account).where(eq(account.userId, userId)); + await tx.delete(promoCodeRedemption).where(eq(promoCodeRedemption.userId, userId)); + await tx.delete(userPortfolio).where(eq(userPortfolio.userId, userId)); + await tx.delete(commentLike).where(eq(commentLike.userId, userId)); + + await tx.update(accountDeletionRequest) + .set({ isProcessed: true }) + .where(eq(accountDeletionRequest.id, request.id)); + + await tx.delete(user).where(eq(user.id, userId)); + }); + + console.log(`✅ Successfully processed account deletion for user ID: ${request.userId}`); + } catch (error: any) { + console.error(`❌ Failed to process account deletion for user ID: ${request.userId}`, error); + + await db.update(accountDeletionRequest) + .set({ + isProcessed: true, // Mark as processed to avoid retries, but log the failure + reason: request.reason ? `${request.reason} - FAILED: ${error.message}` : `FAILED: ${error.message}` + }) + .where(eq(accountDeletionRequest.id, request.id)); + } + } + } catch (error) { + console.error('Error processing account deletions:', error); + } } \ No newline at end of file diff --git a/website/src/lib/types/prediction.ts b/website/src/lib/types/prediction.ts index c327d54..d3187ee 100644 --- a/website/src/lib/types/prediction.ts +++ b/website/src/lib/types/prediction.ts @@ -26,16 +26,17 @@ export interface PredictionQuestion { estimatedYesWinnings?: number; estimatedNoWinnings?: number; }; + // fuck gdpr and all that fucking shit recentBets?: Array<{ - id: number; + id?: number; side: boolean; amount: number; createdAt: string; - user: { - id: number; - name: string; - username: string; - image: string; + user?: { + id?: number; + name?: string; + username?: string; + image?: string; }; }>; } diff --git a/website/src/routes/api/settings/data-download/+server.ts b/website/src/routes/api/settings/data-download/+server.ts new file mode 100644 index 0000000..492746d --- /dev/null +++ b/website/src/routes/api/settings/data-download/+server.ts @@ -0,0 +1,240 @@ +import { auth } from '$lib/auth'; +import { error } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { + user, + transaction, + coin, + userPortfolio, + predictionBet, + predictionQuestion, + comment, + commentLike, + promoCodeRedemption, + promoCode, + session +} from '$lib/server/db/schema'; +import { eq, and, lte } from 'drizzle-orm'; + +export async function HEAD({ request }) { + const authSession = await auth.api.getSession({ + headers: request.headers + }); + + if (!authSession?.user) { + throw error(401, 'Not authenticated'); + } + + const userId = Number(authSession.user.id); + + try { + // Quick check to estimate file size without generating full data + // Get counts of major data types to estimate size + const userExists = await db.select({ id: user.id }) + .from(user) + .where(eq(user.id, userId)) + .limit(1); + + if (!userExists.length) { + throw error(404, 'User not found'); + } + + // Estimate file size based on typical data sizes + // This is a rough estimate - actual size may vary + const estimatedSize = 1024 * 50; // Base 50KB estimate + + return new Response(null, { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'Content-Disposition': `attachment; filename="rugplay-data-${userId}-${new Date().toISOString().split('T')[0]}.json"`, + 'Content-Length': estimatedSize.toString(), + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + } + }); + + } catch (err) { + console.error('Data export HEAD error:', err); + throw error(500, 'Failed to check export availability'); + } +} + +export async function GET({ request }) { + const authSession = await auth.api.getSession({ + headers: request.headers + }); + + if (!authSession?.user) { + throw error(401, 'Not authenticated'); + } + + const userId = Number(authSession.user.id); + + try { + // Get user data + const userData = await db.select() + .from(user) + .where(eq(user.id, userId)) + .limit(1); + + if (!userData.length) { + throw error(404, 'User not found'); + } + + // Get all user's transactions + const transactions = await db.select({ + id: transaction.id, + coinId: transaction.coinId, + coinName: coin.name, + coinSymbol: coin.symbol, + type: transaction.type, + quantity: transaction.quantity, + pricePerCoin: transaction.pricePerCoin, + totalBaseCurrencyAmount: transaction.totalBaseCurrencyAmount, + timestamp: transaction.timestamp + }) + .from(transaction) + .leftJoin(coin, eq(transaction.coinId, coin.id)) + .where(eq(transaction.userId, userId)); + + // Get user's portfolio + const portfolio = await db.select({ + coinId: userPortfolio.coinId, + coinName: coin.name, + coinSymbol: coin.symbol, + quantity: userPortfolio.quantity, + updatedAt: userPortfolio.updatedAt + }) + .from(userPortfolio) + .leftJoin(coin, eq(userPortfolio.coinId, coin.id)) + .where(eq(userPortfolio.userId, userId)); + + // Get user's prediction bets + const predictionBets = await db.select({ + id: predictionBet.id, + questionId: predictionBet.questionId, + question: predictionQuestion.question, + side: predictionBet.side, + amount: predictionBet.amount, + actualWinnings: predictionBet.actualWinnings, + createdAt: predictionBet.createdAt, + settledAt: predictionBet.settledAt + }) + .from(predictionBet) + .leftJoin(predictionQuestion, eq(predictionBet.questionId, predictionQuestion.id)) + .where(eq(predictionBet.userId, userId)); + + // Get user's comments + const comments = await db.select({ + id: comment.id, + coinId: comment.coinId, + coinName: coin.name, + coinSymbol: coin.symbol, + content: comment.content, + likesCount: comment.likesCount, + createdAt: comment.createdAt, + updatedAt: comment.updatedAt, + isDeleted: comment.isDeleted + }) + .from(comment) + .leftJoin(coin, eq(comment.coinId, coin.id)) + .where(eq(comment.userId, userId)); + + // Get user's comment likes + const commentLikes = await db.select({ + commentId: commentLike.commentId, + createdAt: commentLike.createdAt + }) + .from(commentLike) + .where(eq(commentLike.userId, userId)); + + // Get user's promo code redemptions + const promoRedemptions = await db.select({ + id: promoCodeRedemption.id, + promoCodeId: promoCodeRedemption.promoCodeId, + promoCode: promoCode.code, + rewardAmount: promoCodeRedemption.rewardAmount, + redeemedAt: promoCodeRedemption.redeemedAt + }) + .from(promoCodeRedemption) + .leftJoin(promoCode, eq(promoCodeRedemption.promoCodeId, promoCode.id)) + .where(eq(promoCodeRedemption.userId, userId)); + + // Get user's sessions (limited to active ones for privacy) + const sessions = await db.select({ + id: session.id, + expiresAt: session.expiresAt, + createdAt: session.createdAt, + ipAddress: session.ipAddress, + userAgent: session.userAgent + }) + .from(session) + .where(and( + eq(session.userId, userId), + lte(session.expiresAt, new Date()) + )) + + // Get coins created by user + const createdCoins = await db.select({ + id: coin.id, + name: coin.name, + symbol: coin.symbol, + icon: coin.icon, + initialSupply: coin.initialSupply, + circulatingSupply: coin.circulatingSupply, + marketCap: coin.marketCap, + price: coin.currentPrice, + change24h: coin.change24h, + poolCoinAmount: coin.poolCoinAmount, + poolBaseCurrencyAmount: coin.poolBaseCurrencyAmount, + createdAt: coin.createdAt, + updatedAt: coin.updatedAt, + isListed: coin.isListed + }) + .from(coin) + .where(eq(coin.creatorId, userId)); + + // Get questions created by user + const createdQuestions = await db.select() + .from(predictionQuestion) + .where(eq(predictionQuestion.creatorId, userId)); + + // Compile all data + const exportData = { + exportInfo: { + userId: userId, + exportedAt: new Date().toISOString(), + dataType: 'complete_user_data' + }, + user: userData[0], + transactions: transactions, + portfolio: portfolio, + predictionBets: predictionBets, + comments: comments, + commentLikes: commentLikes, + promoCodeRedemptions: promoRedemptions, + sessions: sessions, + createdCoins: createdCoins, + createdQuestions: createdQuestions + }; // Serialize the data + const jsonData = JSON.stringify(exportData, null, 2); + const dataSize = new TextEncoder().encode(jsonData).length; + + // Return as downloadable JSON with proper headers for streaming download + return new Response(jsonData, { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'Content-Disposition': `attachment; filename="rugplay-data-${userId}-${new Date().toISOString().split('T')[0]}.json"`, + 'Content-Length': dataSize.toString(), + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + } + }); + + } catch (err) { + console.error('Data export error:', err); + throw error(500, 'Failed to export user data'); + } +} diff --git a/website/src/routes/api/settings/delete-account/+server.ts b/website/src/routes/api/settings/delete-account/+server.ts new file mode 100644 index 0000000..58d77a6 --- /dev/null +++ b/website/src/routes/api/settings/delete-account/+server.ts @@ -0,0 +1,58 @@ +import { auth } from '$lib/auth'; +import { error, json } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { user, accountDeletionRequest } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; + +export async function POST({ request }) { + const authSession = await auth.api.getSession({ + headers: request.headers + }); + + if (!authSession?.user) { + throw error(401, 'Not authenticated'); + } + + const userId = Number(authSession.user.id); + const body = await request.json(); + const { confirmationText } = body; + + if (confirmationText !== 'DELETE MY ACCOUNT') { + throw error(400, 'Invalid confirmation text'); + } + + const scheduledDeletionAt = new Date(); + scheduledDeletionAt.setDate(scheduledDeletionAt.getDate() + 14); + + await db.transaction(async (tx) => { + const existingRequest = await tx.select() + .from(accountDeletionRequest) + .where(eq(accountDeletionRequest.userId, userId)) + .limit(1); + + if (existingRequest.length > 0) { + throw new Error('Account deletion already requested'); + } + + await tx.insert(accountDeletionRequest).values({ + userId, + scheduledDeletionAt, + reason: 'User requested account deletion' + }); + + await tx.update(user) + .set({ + isBanned: true, + banReason: 'Account deletion requested - scheduled for ' + scheduledDeletionAt.toISOString(), + updatedAt: new Date() + }) + .where(eq(user.id, userId)); + }); + + + return json({ + success: true, + message: `Account deletion has been scheduled for ${scheduledDeletionAt.toLocaleDateString()}. Your account has been temporarily suspended. You can cancel this request by contacting support before the scheduled date.`, + scheduledDeletionAt: scheduledDeletionAt.toISOString() + }); +} \ No newline at end of file diff --git a/website/src/routes/hopium/[id]/+page.svelte b/website/src/routes/hopium/[id]/+page.svelte index a6823bc..1152149 100644 --- a/website/src/routes/hopium/[id]/+page.svelte +++ b/website/src/routes/hopium/[id]/+page.svelte @@ -570,41 +570,50 @@
{#each question.recentBets as bet} -
-
- - - - - - - - - - {bet.side ? 'YES' : 'NO'} - -
-
-
${bet.amount.toFixed(2)}
-
- {new Date(bet.createdAt).toLocaleDateString()} + {#if bet.user} +
+
+ + + + + + {#if bet.user?.id} + + {/if} + + + + {bet.side ? 'YES' : 'NO'} + +
+
+
${bet.amount.toFixed(2)}
+
+ {new Date(bet.createdAt).toLocaleDateString()} +
-
+ {/if} {/each}
diff --git a/website/src/routes/legal/privacy/+page.svelte b/website/src/routes/legal/privacy/+page.svelte new file mode 100644 index 0000000..43e79fb --- /dev/null +++ b/website/src/routes/legal/privacy/+page.svelte @@ -0,0 +1,413 @@ + + + + Privacy Policy - Rugplay + + + + + + + + +
+ +
+ +
+

Privacy Policy

+

+ Last updated: {LAST_UPDATED} +

+
+
+ + + + Important Notice About Account Deletion + + When you delete your account, all personal information is permanently removed, but some + anonymized data may be retained for platform integrity. This is explained in detail below. + + + + + +
+ +

1. Our Privacy Commitment

+

+ We are committed to protecting your privacy while providing a secure and functional crypto + trading simulation platform. This policy explains exactly what data we collect, how we use + it, and what happens when you delete your account. +

+

+ Platform Note: Rugplay is a simulated trading environment using virtual currency + ("*BUSS" or "$") with no real monetary value. +

+
+ + +

2. Information We Collect

+
+
+

2.1 Account Information

+
    +
  • Email address
  • +
  • Username
  • +
  • Profile information (name, bio, profile picture)
  • +
  • Account preferences and settings
  • +
  • Authentication tokens and session data
  • +
  • + IP addresses and browser/user agent information (for security and session + management) +
  • +
+
+ +
+

2.2 Trading and Financial Data (Simulated)

+
    +
  • Transaction history (buy/sell orders, amounts, prices, timestamps)
  • +
  • Portfolio holdings and balances
  • +
  • Trading patterns and preferences
  • +
  • Prediction market bets and outcomes
  • +
  • Reward claims and promotional code usage
  • +
+
+ +
+

2.3 Platform Activity

+
    +
  • Comments and posts on coin pages
  • +
  • Likes and interactions with content
  • +
  • Login activity and streaks
  • + +
+
+
+
+ + +

3. How We Use Your Data

+

We use your data for:

+
    +
  • Providing trading platform functionality
  • +
  • Maintaining accurate portfolio and transaction records
  • +
  • Calculating market prices and liquidity
  • +
  • Fraud prevention and security monitoring
  • +
  • Platform analytics and improvements
  • +
  • Resolving disputes and maintaining system integrity
  • +
  • + Operating and resolving prediction markets, which may involve automated decision-making + as detailed below. +
  • +
+ +

+ 3.1 Automated Decision-Making (Prediction Markets) +

+

+ Our prediction markets are entirely AI-automated systems that determine outcomes of + prediction questions without human intervention. The AI processes publicly available + information and platform data to settle expired prediction questions automatically. +

+

+ No Guarantee of Accuracy: AI decisions are not guaranteed to be correct or + accurate. The automated system may make errors in interpreting data or determining outcomes. +

+

+ Significance and Envisaged Consequences: These automated decisions directly + determine whether your prediction bets are won or lost and the corresponding virtual payouts. + All outcomes are final once processed. +

+

+ Limitation of Liability: We are not responsible for any losses, incorrect + outcomes, or disputes arising from automated AI decisions in prediction markets. By participating, + you acknowledge and accept the risks of AI-automated resolution systems. +

+
+ + +

+ 4. Account Deletion and Data Retention +

+ + + + 14-Day Deletion Process + + Account deletion is scheduled 14 days after your request. During this period, your + account is suspended but you can contact support to cancel the deletion. + + + +
+
+

+ 4.1 What Gets Permanently Deleted +

+
    +
  • Your personal information (name, email, username, bio, profile picture)
  • +
  • + Authentication sessions (including IP address and user agent history) and login + tokens +
  • +
  • OAuth account connections
  • +
  • Account preferences and settings
  • +
  • Portfolio holdings and balances (*BUSS ("$") and simulated coins)
  • +
  • Records of your likes on comments
  • +
  • Promo code redemption records linked to your account
  • +
  • Your user account record itself
  • +
+
+ +
+

+ 4.2 What May Remain (Fully Anonymized) +

+

+ The following data may remain but is completely disconnected from your identity + (your User ID is removed): +

+
    +
  • + Transaction records: Trading history (buys/sells, amounts, prices, timestamps) + with your User ID removed. +
  • +
  • + Comment content: Your comments will have their User ID removed, the + content replaced with "[deleted]", and marked as deleted. Timestamps and like counts + (aggregated on the comment itself) are preserved. +
  • +
  • + Prediction bets: Records of bets placed, with your User ID removed. +
  • +
  • + Created coins: Simulated coins you created will remain, but your User + ID as creator will be removed. +
  • +
  • + Created prediction questions: Prediction questions you created will + remain, but your User ID as creator will be removed. +
  • +
+
+ +
+

4.3 Why Some Data Remains Anonymized

+

Anonymized data serves these legitimate purposes:

+
    +
  • + Market Integrity: Historical transaction data is needed to maintain + accurate price charts and trading history +
  • +
  • + Platform Function: Coins and prediction markets must remain functional + even after creators delete their accounts +
  • +
  • + Community Content: Comments remain available to preserve discussion + threads, but with anonymous authorship +
  • +
  • + System Balance: Transaction records ensure the platform's virtual economy + remains mathematically consistent +
  • +
+

+ Important: This anonymized data cannot be linked back to you in any way + as all personal identifiers are permanently removed. +

+
+ +
+

4.4 Deletion Timeline

+
    +
  • Immediate: Account suspended and login disabled
  • +
  • + 14 days later: Complete deletion process executed automatically +
  • +
  • + Cancellation: Contact support within 14 days to cancel deletion +
  • +
+
+
+
+ + +

5. Data Security

+

We implement industry-standard security measures including:

+
    +
  • Encryption of sensitive data in transit and at rest
  • +
  • Regular security audits and monitoring
  • +
  • Access controls and authentication requirements
  • +
  • Secure database configurations and backups
  • +
+
+ + +

6. Your Data Protection Rights

+

+ Depending on your location, you may have certain rights regarding your personal data, + including: +

+
    +
  • Access: The right to access the personal data we hold about you.
  • +
  • + Rectification: The right to correct inaccurate or incomplete data. +
  • +
  • + Erasure (Deletion): The right to request deletion of your personal data + (subject to our retention policy and legal obligations, as described in Section 4). +
  • +
  • + Portability: The right to export your data in a structured, commonly used, + and machine-readable format. +
  • +
  • + Restrict Processing: The right to request restriction of processing your + personal data under certain conditions. +
  • +
  • + Object to Processing: The right to object to processing of your personal + data under certain conditions, including for direct marketing purposes (which we do not currently + engage in). +
  • +
  • + Rights related to automated decision-making: The right not to be subject + to a decision based solely on automated processing, including profiling, which produces legal + effects concerning you or similarly significantly affects you, except under certain conditions. + As described in Section 3.1, we use automated decision-making for prediction market resolution, + and we provide information and recourse options. +
  • +
+

+ To exercise these rights, please contact us at {CONTACT_EMAIL}. +

+
+ + +

7. Consent and Legal Basis

+
+
+

7.1 Explicit Consent Required

+

By creating an account, you explicitly consent to:

+
    +
  • Our data collection and processing practices
  • +
  • The anonymization and retention policy described in Section 4
  • +
  • Processing of your trading data for platform functionality
  • +
  • 14-day deletion process with suspension during the waiting period
  • +
+
+ +
+

7.2 Withdrawal of Consent

+

You can withdraw consent at any time by deleting your account.

+
+
+
+ + +

8. Data Sharing

+

We do not sell or share your personal data with third parties, except:

+
    +
  • When required by law or legal process
  • +
  • To prevent fraud or protect platform security
  • +
  • + With service providers who assist in platform operations (under strict data processing + agreements) +
  • +
+
+ + +

9. International Data Transfers

+

+ Your data may be processed outside the EU. We ensure adequate protection through standard + contractual clauses and appropriate safeguards as required by GDPR. +

+
+ +

10. Cookies and Tracking Technologies

+

+ We use cookies solely for authentication and session management purposes. These essential + cookies are necessary to maintain your login state and provide secure access to your + account. +

+
+ + +

11. Contact and Complaints

+

For privacy-related questions or to exercise your rights:

+
    +
  • + Email: {CONTACT_EMAIL} +
  • +
  • To cancel account deletion: Contact us immediately at the above email
  • +
  • You have the right to lodge a complaint with your local data protection authority
  • +
+
+ + +

12. Policy Updates

+

+ We will notify you of material changes to this policy via email and platform + notifications. Continued use after changes constitutes acceptance of the updated policy. +

+
+ +
+

Last Updated: {LAST_UPDATED}

+

+ Contact: + {CONTACT_EMAIL} +

+

Platform: Rugplay - virtual cryptocurrency trading simulation

+
+
+ + + +
+ + +
+
+
diff --git a/website/src/routes/settings/+page.svelte b/website/src/routes/settings/+page.svelte index 5eddefb..f2bb1c0 100644 --- a/website/src/routes/settings/+page.svelte +++ b/website/src/routes/settings/+page.svelte @@ -9,11 +9,12 @@ import * as Card from '$lib/components/ui/card'; import { Slider } from '$lib/components/ui/slider'; import { onMount, onDestroy } from 'svelte'; - import { CheckIcon, Volume2Icon, VolumeXIcon } from 'lucide-svelte'; + import { CheckIcon, Volume2Icon, VolumeXIcon, DownloadIcon, Trash2Icon } from 'lucide-svelte'; import { toast } from 'svelte-sonner'; import { MAX_FILE_SIZE } from '$lib/data/constants'; import { volumeSettings } from '$lib/stores/volume-settings'; import { USER_DATA } from '$lib/stores/user-data'; + import * as Dialog from '$lib/components/ui/dialog'; let name = $state($USER_DATA?.name || ''); let bio = $state($USER_DATA?.bio ?? ''); @@ -37,10 +38,14 @@ let loading = $state(false); let usernameAvailable: boolean | null = $state(null); let checkingUsername = $state(false); - let masterVolume = $state(($USER_DATA?.volumeMaster || 0) * 100); let isMuted = $state($USER_DATA?.volumeMuted || false); + let deleteDialogOpen = $state(false); + let deleteConfirmationText = $state(''); + let isDeleting = $state(false); + let isDownloading = $state(false); + function beforeUnloadHandler(e: BeforeUnloadEvent) { if (isDirty) { e.preventDefault(); @@ -155,12 +160,103 @@ volumeSettings.setMaster(normalizedValue); saveVolumeToServer({ master: normalizedValue, muted: isMuted }); } - function toggleMute() { isMuted = !isMuted; volumeSettings.setMuted(isMuted); saveVolumeToServer({ master: masterVolume / 100, muted: isMuted }); } + + async function downloadUserData() { + isDownloading = true; + try { + const headResponse = await fetch('/api/settings/data-download', { + method: 'HEAD' + }); + + if (!headResponse.ok) { + throw new Error('Download service unavailable'); + } + + const contentLength = headResponse.headers.get('Content-Length'); + if (contentLength) { + const sizeInMB = parseInt(contentLength) / (1024 * 1024); + if (sizeInMB > 50) { + const proceed = confirm( + `Your data export is ${sizeInMB.toFixed(1)}MB. This may take a while to download. Continue?` + ); + if (!proceed) { + isDownloading = false; + return; + } + } + } + + const downloadUrl = '/api/settings/data-download'; + + const downloadWindow = window.open(downloadUrl, '_blank'); + + if (!downloadWindow || downloadWindow.closed) { + const a = document.createElement('a'); + a.href = downloadUrl; + a.style.display = 'none'; + a.target = '_blank'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } else { + setTimeout(() => { + try { + downloadWindow.close(); + } catch (e) {} + }, 1000); + } + + toast.success('Your data download has started'); + } catch (error) { + console.error('Download error:', error); + toast.error('Failed to start data download: ' + (error as Error).message); + } finally { + isDownloading = false; + } + } + + async function deleteAccount() { + if (deleteConfirmationText !== 'DELETE MY ACCOUNT') { + toast.error('Please type "DELETE MY ACCOUNT" to confirm'); + return; + } + + isDeleting = true; + try { + const response = await fetch('/api/settings/delete-account', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + confirmationText: deleteConfirmationText + }) + }); + + if (!response.ok) { + const result = await response.json(); + throw new Error(result.message || 'Failed to delete account'); + } + + toast.success('Account deleted successfully. You will be logged out shortly.'); + + setTimeout(() => { + window.location.href = '/'; + }, 2000); + } catch (error: any) { + console.error('Delete account error:', error); + toast.error('Failed to delete account: ' + error.message); + } finally { + isDeleting = false; + deleteDialogOpen = false; + deleteConfirmationText = ''; + } + }
@@ -289,5 +385,99 @@
+ + + + Data & Privacy + Manage your personal data and account + + +
+
+
+

Download Your Data

+

+ Export a complete copy of your account data including transactions, bets, and + profile information. +

+
+ +
+ +
+
+

Delete Account

+

+ Permanently delete your account. This will anonymize your data while preserving + transaction records for compliance. +

+
+ +
+
+
+
+ + + + + Delete Account + + This action cannot be undone. Your account will be permanently deleted and your data will be + anonymized. + + +
+
+

What happens when you delete your account:

+
    +
  • • Your profile information will be permanently removed
  • +
  • • You will be logged out from all devices
  • +
  • • Your comments will be anonymized
  • +
  • • Transaction history will be preserved for compliance (anonymized)
  • +
  • • You will not be able to recover this account
  • +
+
+
+ + +
+
+ + + + +
+