feat: api
This commit is contained in:
parent
ee29f97ca4
commit
45a49e3f2f
29 changed files with 1622 additions and 5532 deletions
|
|
@ -42,6 +42,30 @@ CREATE TABLE IF NOT EXISTS "account_deletion_request" (
|
||||||
CONSTRAINT "account_deletion_request_user_id_unique" UNIQUE("user_id")
|
CONSTRAINT "account_deletion_request_user_id_unique" UNIQUE("user_id")
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "apikey" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"name" text,
|
||||||
|
"start" text,
|
||||||
|
"prefix" text,
|
||||||
|
"key" text NOT NULL,
|
||||||
|
"user_id" integer NOT NULL,
|
||||||
|
"refill_interval" integer,
|
||||||
|
"refill_amount" integer,
|
||||||
|
"last_refill_at" timestamp,
|
||||||
|
"enabled" boolean,
|
||||||
|
"rate_limit_enabled" boolean,
|
||||||
|
"rate_limit_time_window" integer,
|
||||||
|
"rate_limit_max" integer,
|
||||||
|
"request_count" integer,
|
||||||
|
"remaining" integer,
|
||||||
|
"last_request" timestamp,
|
||||||
|
"expires_at" timestamp,
|
||||||
|
"created_at" timestamp NOT NULL,
|
||||||
|
"updated_at" timestamp NOT NULL,
|
||||||
|
"permissions" text,
|
||||||
|
"metadata" text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
CREATE TABLE IF NOT EXISTS "coin" (
|
CREATE TABLE IF NOT EXISTS "coin" (
|
||||||
"id" serial PRIMARY KEY NOT NULL,
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
"name" varchar(255) NOT NULL,
|
"name" varchar(255) NOT NULL,
|
||||||
|
|
@ -189,6 +213,7 @@ CREATE TABLE IF NOT EXISTS "user" (
|
||||||
"last_reward_claim" timestamp with time zone,
|
"last_reward_claim" timestamp with time zone,
|
||||||
"total_rewards_claimed" numeric(20, 8) DEFAULT '0.00000000' NOT NULL,
|
"total_rewards_claimed" numeric(20, 8) DEFAULT '0.00000000' NOT NULL,
|
||||||
"login_streak" integer DEFAULT 0 NOT NULL,
|
"login_streak" integer DEFAULT 0 NOT NULL,
|
||||||
|
"prestige_level" integer DEFAULT 0,
|
||||||
CONSTRAINT "user_email_unique" UNIQUE("email"),
|
CONSTRAINT "user_email_unique" UNIQUE("email"),
|
||||||
CONSTRAINT "user_username_unique" UNIQUE("username")
|
CONSTRAINT "user_username_unique" UNIQUE("username")
|
||||||
);
|
);
|
||||||
|
|
@ -222,6 +247,12 @@ EXCEPTION
|
||||||
WHEN duplicate_object THEN null;
|
WHEN duplicate_object THEN null;
|
||||||
END $$;
|
END $$;
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "apikey" ADD CONSTRAINT "apikey_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
|
DO $$ BEGIN
|
||||||
ALTER TABLE "coin" ADD CONSTRAINT "coin_creator_id_user_id_fk" FOREIGN KEY ("creator_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;
|
ALTER TABLE "coin" ADD CONSTRAINT "coin_creator_id_user_id_fk" FOREIGN KEY ("creator_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;
|
||||||
EXCEPTION
|
EXCEPTION
|
||||||
|
|
@ -345,6 +376,15 @@ END $$;
|
||||||
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_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
|
CREATE INDEX IF NOT EXISTS "account_deletion_request_scheduled_deletion_idx" ON "account_deletion_request" USING btree ("scheduled_deletion_at");--> statement-breakpoint
|
||||||
CREATE INDEX IF NOT EXISTS "account_deletion_request_open_idx" ON "account_deletion_request" USING btree ("user_id") WHERE is_processed = false;--> statement-breakpoint
|
CREATE INDEX IF NOT EXISTS "account_deletion_request_open_idx" ON "account_deletion_request" USING btree ("user_id") WHERE is_processed = false;--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_apikey_user" ON "apikey" USING btree ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "coin_symbol_idx" ON "coin" USING btree ("symbol");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "coin_creator_id_idx" ON "coin" USING btree ("creator_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "coin_is_listed_idx" ON "coin" USING btree ("is_listed");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "coin_market_cap_idx" ON "coin" USING btree ("market_cap");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "coin_current_price_idx" ON "coin" USING btree ("current_price");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "coin_change24h_idx" ON "coin" USING btree ("change_24h");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "coin_volume24h_idx" ON "coin" USING btree ("volume_24h");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "coin_created_at_idx" ON "coin" USING btree ("created_at");--> statement-breakpoint
|
||||||
CREATE INDEX IF NOT EXISTS "comment_user_id_idx" ON "comment" USING btree ("user_id");--> statement-breakpoint
|
CREATE INDEX IF NOT EXISTS "comment_user_id_idx" ON "comment" USING btree ("user_id");--> statement-breakpoint
|
||||||
CREATE INDEX IF NOT EXISTS "comment_coin_id_idx" ON "comment" USING btree ("coin_id");--> statement-breakpoint
|
CREATE INDEX IF NOT EXISTS "comment_coin_id_idx" ON "comment" USING btree ("coin_id");--> statement-breakpoint
|
||||||
CREATE INDEX IF NOT EXISTS "notification_user_id_idx" ON "notification" USING btree ("user_id");--> statement-breakpoint
|
CREATE INDEX IF NOT EXISTS "notification_user_id_idx" ON "notification" USING btree ("user_id");--> statement-breakpoint
|
||||||
|
|
@ -357,4 +397,16 @@ CREATE INDEX IF NOT EXISTS "prediction_bet_user_question_idx" ON "prediction_bet
|
||||||
CREATE INDEX IF NOT EXISTS "prediction_bet_created_at_idx" ON "prediction_bet" USING btree ("created_at");--> statement-breakpoint
|
CREATE INDEX IF NOT EXISTS "prediction_bet_created_at_idx" ON "prediction_bet" USING btree ("created_at");--> statement-breakpoint
|
||||||
CREATE INDEX IF NOT EXISTS "prediction_question_creator_id_idx" ON "prediction_question" USING btree ("creator_id");--> statement-breakpoint
|
CREATE INDEX IF NOT EXISTS "prediction_question_creator_id_idx" ON "prediction_question" USING btree ("creator_id");--> statement-breakpoint
|
||||||
CREATE INDEX IF NOT EXISTS "prediction_question_status_idx" ON "prediction_question" USING btree ("status");--> statement-breakpoint
|
CREATE INDEX IF NOT EXISTS "prediction_question_status_idx" ON "prediction_question" USING btree ("status");--> statement-breakpoint
|
||||||
CREATE INDEX IF NOT EXISTS "prediction_question_resolution_date_idx" ON "prediction_question" USING btree ("resolution_date");
|
CREATE INDEX IF NOT EXISTS "prediction_question_resolution_date_idx" ON "prediction_question" USING btree ("resolution_date");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "prediction_question_status_resolution_idx" ON "prediction_question" USING btree ("status","resolution_date");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "transaction_user_id_idx" ON "transaction" USING btree ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "transaction_coin_id_idx" ON "transaction" USING btree ("coin_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "transaction_type_idx" ON "transaction" USING btree ("type");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "transaction_timestamp_idx" ON "transaction" USING btree ("timestamp");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "transaction_user_coin_idx" ON "transaction" USING btree ("user_id","coin_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "transaction_coin_type_idx" ON "transaction" USING btree ("coin_id","type");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_username_idx" ON "user" USING btree ("username");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_is_banned_idx" ON "user" USING btree ("is_banned");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_is_admin_idx" ON "user" USING btree ("is_admin");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_created_at_idx" ON "user" USING btree ("created_at");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_updated_at_idx" ON "user" USING btree ("updated_at");
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
CREATE INDEX IF NOT EXISTS "prediction_question_status_resolution_idx" ON "prediction_question" USING btree ("status","resolution_date");
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE "user" ADD COLUMN "prestige_level" integer DEFAULT 0;
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
CREATE INDEX IF NOT EXISTS "coin_symbol_idx" ON "coin" USING btree ("symbol");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "coin_creator_id_idx" ON "coin" USING btree ("creator_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "coin_is_listed_idx" ON "coin" USING btree ("is_listed");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "coin_market_cap_idx" ON "coin" USING btree ("market_cap");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "coin_current_price_idx" ON "coin" USING btree ("current_price");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "coin_change24h_idx" ON "coin" USING btree ("change_24h");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "coin_volume24h_idx" ON "coin" USING btree ("volume_24h");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "coin_created_at_idx" ON "coin" USING btree ("created_at");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "transaction_user_id_idx" ON "transaction" USING btree ("user_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "transaction_coin_id_idx" ON "transaction" USING btree ("coin_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "transaction_type_idx" ON "transaction" USING btree ("type");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "transaction_timestamp_idx" ON "transaction" USING btree ("timestamp");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "transaction_user_coin_idx" ON "transaction" USING btree ("user_id","coin_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "transaction_coin_type_idx" ON "transaction" USING btree ("coin_id","type");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "user_username_idx" ON "user" USING btree ("username");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "user_is_banned_idx" ON "user" USING btree ("is_banned");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "user_is_admin_idx" ON "user" USING btree ("is_admin");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "user_created_at_idx" ON "user" USING btree ("created_at");--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS "user_updated_at_idx" ON "user" USING btree ("updated_at");
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"id": "41f7bba3-1d5d-41ba-83bb-ca129ace81f0",
|
"id": "08d1623b-7b2d-4777-8b7b-dbfa7884ecfe",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
|
|
@ -225,6 +225,172 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"public.apikey": {
|
||||||
|
"name": "apikey",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"name": "start",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"prefix": {
|
||||||
|
"name": "prefix",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"refill_interval": {
|
||||||
|
"name": "refill_interval",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refill_amount": {
|
||||||
|
"name": "refill_amount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"last_refill_at": {
|
||||||
|
"name": "last_refill_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"name": "enabled",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"rate_limit_enabled": {
|
||||||
|
"name": "rate_limit_enabled",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"rate_limit_time_window": {
|
||||||
|
"name": "rate_limit_time_window",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"rate_limit_max": {
|
||||||
|
"name": "rate_limit_max",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"request_count": {
|
||||||
|
"name": "request_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"remaining": {
|
||||||
|
"name": "remaining",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"last_request": {
|
||||||
|
"name": "last_request",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"name": "permissions",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"name": "metadata",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_apikey_user": {
|
||||||
|
"name": "idx_apikey_user",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"apikey_user_id_user_id_fk": {
|
||||||
|
"name": "apikey_user_id_user_id_fk",
|
||||||
|
"tableFrom": "apikey",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
"public.coin": {
|
"public.coin": {
|
||||||
"name": "coin",
|
"name": "coin",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
|
|
@ -333,7 +499,128 @@
|
||||||
"default": true
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {
|
||||||
|
"coin_symbol_idx": {
|
||||||
|
"name": "coin_symbol_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "symbol",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"coin_creator_id_idx": {
|
||||||
|
"name": "coin_creator_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "creator_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"coin_is_listed_idx": {
|
||||||
|
"name": "coin_is_listed_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "is_listed",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"coin_market_cap_idx": {
|
||||||
|
"name": "coin_market_cap_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "market_cap",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"coin_current_price_idx": {
|
||||||
|
"name": "coin_current_price_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "current_price",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"coin_change24h_idx": {
|
||||||
|
"name": "coin_change24h_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "change_24h",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"coin_volume24h_idx": {
|
||||||
|
"name": "coin_volume24h_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "volume_24h",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"coin_created_at_idx": {
|
||||||
|
"name": "coin_created_at_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "created_at",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"coin_creator_id_user_id_fk": {
|
"coin_creator_id_user_id_fk": {
|
||||||
"name": "coin_creator_id_user_id_fk",
|
"name": "coin_creator_id_user_id_fk",
|
||||||
|
|
@ -955,6 +1242,27 @@
|
||||||
"concurrently": false,
|
"concurrently": false,
|
||||||
"method": "btree",
|
"method": "btree",
|
||||||
"with": {}
|
"with": {}
|
||||||
|
},
|
||||||
|
"prediction_question_status_resolution_idx": {
|
||||||
|
"name": "prediction_question_status_resolution_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "status",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "resolution_date",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
|
|
@ -1338,7 +1646,110 @@
|
||||||
"notNull": false
|
"notNull": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {
|
||||||
|
"transaction_user_id_idx": {
|
||||||
|
"name": "transaction_user_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"transaction_coin_id_idx": {
|
||||||
|
"name": "transaction_coin_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "coin_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"transaction_type_idx": {
|
||||||
|
"name": "transaction_type_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "type",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"transaction_timestamp_idx": {
|
||||||
|
"name": "transaction_timestamp_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "timestamp",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"transaction_user_coin_idx": {
|
||||||
|
"name": "transaction_user_coin_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "coin_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"transaction_coin_type_idx": {
|
||||||
|
"name": "transaction_coin_type_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "coin_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "type",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"transaction_user_id_user_id_fk": {
|
"transaction_user_id_user_id_fk": {
|
||||||
"name": "transaction_user_id_user_id_fk",
|
"name": "transaction_user_id_user_id_fk",
|
||||||
|
|
@ -1518,9 +1929,92 @@
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": 0
|
"default": 0
|
||||||
|
},
|
||||||
|
"prestige_level": {
|
||||||
|
"name": "prestige_level",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_username_idx": {
|
||||||
|
"name": "user_username_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "username",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"user_is_banned_idx": {
|
||||||
|
"name": "user_is_banned_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "is_banned",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"user_is_admin_idx": {
|
||||||
|
"name": "user_is_admin_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "is_admin",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"user_created_at_idx": {
|
||||||
|
"name": "user_created_at_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "created_at",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"user_updated_at_idx": {
|
||||||
|
"name": "user_updated_at_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "updated_at",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
"foreignKeys": {},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {
|
"uniqueConstraints": {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -5,29 +5,8 @@
|
||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1749654046953,
|
"when": 1750863600119,
|
||||||
"tag": "0000_crazy_bloodstrike",
|
"tag": "0000_chief_korath",
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 1,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1749907594739,
|
|
||||||
"tag": "0001_cuddly_dormammu",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 2,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1749916220202,
|
|
||||||
"tag": "0002_small_micromacro",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 3,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1750707307426,
|
|
||||||
"tag": "0003_complete_runaways",
|
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { db } from "./server/db";
|
||||||
import * as schema from "./server/db/schema";
|
import * as schema from "./server/db/schema";
|
||||||
import { generateUsername } from "./utils/random";
|
import { generateUsername } from "./utils/random";
|
||||||
import { uploadProfilePicture } from "./server/s3";
|
import { uploadProfilePicture } from "./server/s3";
|
||||||
|
import { apiKey } from "better-auth/plugins";
|
||||||
|
|
||||||
if (!env.GOOGLE_CLIENT_ID) throw new Error('GOOGLE_CLIENT_ID is not set');
|
if (!env.GOOGLE_CLIENT_ID) throw new Error('GOOGLE_CLIENT_ID is not set');
|
||||||
if (!env.GOOGLE_CLIENT_SECRET) throw new Error('GOOGLE_CLIENT_SECRET is not set');
|
if (!env.GOOGLE_CLIENT_SECRET) throw new Error('GOOGLE_CLIENT_SECRET is not set');
|
||||||
|
|
@ -19,6 +20,21 @@ export const auth = betterAuth({
|
||||||
env.BETTER_AUTH_URL, "http://rugplay.com", "http://localhost:5173",
|
env.BETTER_AUTH_URL, "http://rugplay.com", "http://localhost:5173",
|
||||||
],
|
],
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
apiKey({
|
||||||
|
defaultPrefix: 'rgpl_',
|
||||||
|
rateLimit: {
|
||||||
|
enabled: true,
|
||||||
|
timeWindow: 1000 * 60 * 60 * 24, // 1 day
|
||||||
|
maxRequests: 2000 // 2000 requests per day
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
defaultPermissions: {
|
||||||
|
api: ['read']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
provider: "pg",
|
provider: "pg",
|
||||||
schema: schema,
|
schema: schema,
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@
|
||||||
BookOpen,
|
BookOpen,
|
||||||
Info,
|
Info,
|
||||||
Bell,
|
Bell,
|
||||||
Crown
|
Crown,
|
||||||
|
Key
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { mode, setMode } from 'mode-watcher';
|
import { mode, setMode } from 'mode-watcher';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
@ -158,6 +159,11 @@
|
||||||
goto('/prestige');
|
goto('/prestige');
|
||||||
setOpenMobile(false);
|
setOpenMobile(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAPIClick(){
|
||||||
|
goto('/api');
|
||||||
|
setOpenMobile(false);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SignInConfirmDialog bind:open={shouldSignIn} />
|
<SignInConfirmDialog bind:open={shouldSignIn} />
|
||||||
|
|
@ -411,7 +417,7 @@
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenu.Label>
|
</DropdownMenu.Label>
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
|
|
||||||
<!-- Profile & Settings Group -->
|
<!-- Profile & Settings Group -->
|
||||||
<DropdownMenu.Group>
|
<DropdownMenu.Group>
|
||||||
<DropdownMenu.Item onclick={handleAccountClick}>
|
<DropdownMenu.Item onclick={handleAccountClick}>
|
||||||
|
|
@ -427,11 +433,17 @@
|
||||||
Prestige
|
Prestige
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Group>
|
</DropdownMenu.Group>
|
||||||
|
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
|
|
||||||
<!-- Features Group -->
|
<!-- Features Group -->
|
||||||
<DropdownMenu.Group>
|
<DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onclick={handleAPIClick}
|
||||||
|
>
|
||||||
|
<Key />
|
||||||
|
API
|
||||||
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
showPromoCode = true;
|
showPromoCode = true;
|
||||||
|
|
@ -483,9 +495,9 @@
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Group>
|
</DropdownMenu.Group>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
|
|
||||||
<!-- Legal Group -->
|
<!-- Legal Group -->
|
||||||
<DropdownMenu.Group>
|
<DropdownMenu.Group>
|
||||||
<DropdownMenu.Item onclick={handleTermsClick}>
|
<DropdownMenu.Item onclick={handleTermsClick}>
|
||||||
|
|
@ -497,9 +509,9 @@
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Group>
|
</DropdownMenu.Group>
|
||||||
|
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
|
|
||||||
<!-- Sign Out -->
|
<!-- Sign Out -->
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
|
|
|
||||||
47
website/src/lib/components/self/Codeblock.svelte
Normal file
47
website/src/lib/components/self/Codeblock.svelte
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script>
|
||||||
|
import { scale } from 'svelte/transition';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import Check from 'lucide-svelte/icons/check';
|
||||||
|
import Copy from 'lucide-svelte/icons/copy';
|
||||||
|
|
||||||
|
const { text = '', displayOnly = false } = $props();
|
||||||
|
let isSuccess = $state(false);
|
||||||
|
|
||||||
|
async function copyToClipboard() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
isSuccess = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
isSuccess = false;
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy text:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex w-full items-center gap-2 overflow-hidden rounded-md border bg-primary/10">
|
||||||
|
<code class="block flex-grow overflow-x-auto whitespace-pre-wrap p-3 font-mono text-sm">
|
||||||
|
{text}
|
||||||
|
</code>
|
||||||
|
|
||||||
|
{#if !displayOnly}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="mr-1 h-8 w-8 flex-shrink-0 p-0 hover:bg-primary/15"
|
||||||
|
onclick={copyToClipboard}
|
||||||
|
aria-label="Copy to clipboard"
|
||||||
|
>
|
||||||
|
{#if isSuccess}
|
||||||
|
<div in:scale|fade={{ duration: 150 }}>
|
||||||
|
<Check class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div in:scale|fade={{ duration: 150 }}>
|
||||||
|
<Copy class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.ContentProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CollapsiblePrimitive.Content bind:ref data-slot="collapsible-content" {...restProps} />
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CollapsiblePrimitive.Trigger bind:ref data-slot="collapsible-trigger" {...restProps} />
|
||||||
11
website/src/lib/components/ui/collapsible/collapsible.svelte
Normal file
11
website/src/lib/components/ui/collapsible/collapsible.svelte
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
open = $bindable(false),
|
||||||
|
...restProps
|
||||||
|
}: CollapsiblePrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CollapsiblePrimitive.Root bind:ref bind:open data-slot="collapsible" {...restProps} />
|
||||||
13
website/src/lib/components/ui/collapsible/index.ts
Normal file
13
website/src/lib/components/ui/collapsible/index.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Root from "./collapsible.svelte";
|
||||||
|
import Trigger from "./collapsible-trigger.svelte";
|
||||||
|
import Content from "./collapsible-content.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Trigger,
|
||||||
|
//
|
||||||
|
Root as Collapsible,
|
||||||
|
Content as CollapsibleContent,
|
||||||
|
Trigger as CollapsibleTrigger,
|
||||||
|
};
|
||||||
20
website/src/lib/server/api-auth.ts
Normal file
20
website/src/lib/server/api-auth.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { auth } from "$lib/auth";
|
||||||
|
import { error } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export async function verifyApiKeyAndGetUser(request: Request) {
|
||||||
|
const authHeader = request.headers.get('Authorization');
|
||||||
|
if (!authHeader?.startsWith('Bearer ')) {
|
||||||
|
throw error(401, 'API key required. Use Authorization: Bearer <api-key>');
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKeyStr = authHeader.substring(7);
|
||||||
|
const { valid, error: verifyError, key } = await auth.api.verifyApiKey({
|
||||||
|
body: { key: apiKeyStr }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (verifyError || !valid || !key) {
|
||||||
|
throw error(401, 'Invalid API key');
|
||||||
|
}
|
||||||
|
|
||||||
|
return key.userId;
|
||||||
|
}
|
||||||
|
|
@ -274,4 +274,30 @@ export const notifications = pgTable("notification", {
|
||||||
isReadIdx: index("notification_is_read_idx").on(table.isRead),
|
isReadIdx: index("notification_is_read_idx").on(table.isRead),
|
||||||
createdAtIdx: index("notification_created_at_idx").on(table.createdAt),
|
createdAtIdx: index("notification_created_at_idx").on(table.createdAt),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const apikey = pgTable("apikey", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
name: text('name'),
|
||||||
|
start: text('start'),
|
||||||
|
prefix: text('prefix'),
|
||||||
|
key: text('key').notNull(),
|
||||||
|
userId: integer('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
|
||||||
|
refillInterval: integer('refill_interval'),
|
||||||
|
refillAmount: integer('refill_amount'),
|
||||||
|
lastRefillAt: timestamp('last_refill_at'),
|
||||||
|
enabled: boolean('enabled'),
|
||||||
|
rateLimitEnabled: boolean('rate_limit_enabled'),
|
||||||
|
rateLimitTimeWindow: integer('rate_limit_time_window'),
|
||||||
|
rateLimitMax: integer('rate_limit_max'),
|
||||||
|
requestCount: integer('request_count'),
|
||||||
|
remaining: integer('remaining'),
|
||||||
|
lastRequest: timestamp('last_request'),
|
||||||
|
expiresAt: timestamp('expires_at'),
|
||||||
|
createdAt: timestamp('created_at').notNull(),
|
||||||
|
updatedAt: timestamp('updated_at').notNull(),
|
||||||
|
permissions: text('permissions'),
|
||||||
|
metadata: text('metadata')
|
||||||
|
}, (table) => ({
|
||||||
|
userIdx: index("idx_apikey_user").on(table.userId)
|
||||||
|
}));
|
||||||
20
website/src/routes/api/+page.server.ts
Normal file
20
website/src/routes/api/+page.server.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { auth } from '$lib/auth';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async (event) => {
|
||||||
|
const session = await auth.api.getSession({ headers: event.request.headers });
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return { apiKey: null, todayUsage: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = await auth.api.listApiKeys({ headers: event.request.headers });
|
||||||
|
const key = keys.length > 0 ? keys[0] : null;
|
||||||
|
|
||||||
|
const todayUsage = key ? 2000 - (key.remaining || 0) : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiKey: key,
|
||||||
|
todayUsage
|
||||||
|
};
|
||||||
|
};
|
||||||
650
website/src/routes/api/+page.svelte
Normal file
650
website/src/routes/api/+page.svelte
Normal file
|
|
@ -0,0 +1,650 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||||
|
import { Progress } from '$lib/components/ui/progress';
|
||||||
|
import { Alert, AlertDescription } from '$lib/components/ui/alert';
|
||||||
|
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||||
|
|
||||||
|
import Key from 'lucide-svelte/icons/key';
|
||||||
|
import Activity from 'lucide-svelte/icons/activity';
|
||||||
|
import AlertTriangle from 'lucide-svelte/icons/alert-triangle';
|
||||||
|
import ChevronDown from 'lucide-svelte/icons/chevron-down';
|
||||||
|
import ChevronRight from 'lucide-svelte/icons/chevron-right';
|
||||||
|
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import Codeblock from '$lib/components/self/Codeblock.svelte';
|
||||||
|
import { USER_DATA } from '$lib/stores/user-data';
|
||||||
|
import SignInConfirmDialog from '$lib/components/self/SignInConfirmDialog.svelte';
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
let apiKey = $state(null);
|
||||||
|
let apiKeyId = $state<string | null>(data.apiKey?.id || null);
|
||||||
|
let justCreated = $state(false);
|
||||||
|
let credits = $state(data.apiKey?.remaining || 0);
|
||||||
|
let todayUsage = $state(data.todayUsage || 0);
|
||||||
|
let shouldSignIn = $state(false);
|
||||||
|
|
||||||
|
const maxDailyRequests = 2000;
|
||||||
|
const usagePercentage = $derived((todayUsage / maxDailyRequests) * 100);
|
||||||
|
|
||||||
|
let loading = $state(false);
|
||||||
|
|
||||||
|
// State for collapsible sections
|
||||||
|
let authOpen = $state(false);
|
||||||
|
let topCoinsOpen = $state(false);
|
||||||
|
let marketDataOpen = $state(false);
|
||||||
|
let coinDetailsOpen = $state(false);
|
||||||
|
let holdersOpen = $state(false);
|
||||||
|
let hopiumOpen = $state(false);
|
||||||
|
let hopiumDetailsOpen = $state(false);
|
||||||
|
let rateLimitingOpen = $state(false);
|
||||||
|
let errorResponsesOpen = $state(false);
|
||||||
|
|
||||||
|
async function createKey() {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/keys', {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to create key');
|
||||||
|
const { id, key, remaining } = await response.json();
|
||||||
|
apiKeyId = id;
|
||||||
|
apiKey = key;
|
||||||
|
credits = remaining;
|
||||||
|
|
||||||
|
justCreated = true;
|
||||||
|
toast.success('API key created');
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('Failed to create API key');
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function regenerateKey() {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/keys/${apiKeyId}/regenerate`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to regenerate key');
|
||||||
|
const { id, key, remaining } = await response.json();
|
||||||
|
apiKeyId = id;
|
||||||
|
apiKey = key;
|
||||||
|
credits = remaining;
|
||||||
|
justCreated = true;
|
||||||
|
toast.success('API key regenerated');
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('Failed to regenerate key');
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SignInConfirmDialog bind:open={shouldSignIn} />
|
||||||
|
|
||||||
|
<div class="container mx-auto max-w-4xl space-y-6 p-4 md:p-8">
|
||||||
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<h1 class="text-3xl font-bold">API Access</h1>
|
||||||
|
<p class="text-muted-foreground">Manage your API access and usage</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !$USER_DATA}
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex flex-col items-center justify-center py-12">
|
||||||
|
<Key class="text-muted-foreground mb-4 h-12 w-12" />
|
||||||
|
<h3 class="mb-2 text-lg font-semibold">Sign in required</h3>
|
||||||
|
<p class="text-muted-foreground mb-4 text-center">
|
||||||
|
Sign in to get your free API key.
|
||||||
|
</p>
|
||||||
|
<Button onclick={() => (shouldSignIn = true)}>Sign In</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
{:else}
|
||||||
|
<!-- Usage Card -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="flex items-center gap-2">
|
||||||
|
<Activity class="h-5 w-5" />
|
||||||
|
Today's Usage
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span>{todayUsage.toLocaleString()} requests</span>
|
||||||
|
<span>{maxDailyRequests.toLocaleString()} max</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={usagePercentage} class="h-2" />
|
||||||
|
<p class="text-muted-foreground text-xs">
|
||||||
|
{Math.max(0, maxDailyRequests - todayUsage).toLocaleString()} requests remaining today
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- API Key Management -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>API Key</CardTitle>
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
Use this key to authenticate your API requests
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{#if apiKeyId}
|
||||||
|
<Button variant="outline" onclick={regenerateKey} disabled={loading}>
|
||||||
|
<Key class="h-4 w-4" />
|
||||||
|
Regenerate
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{#if apiKey && justCreated}
|
||||||
|
<div class="space-y-4">
|
||||||
|
<Codeblock text={apiKey} />
|
||||||
|
<Alert>
|
||||||
|
<AlertTriangle class="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
This is the only time your full API key will be shown. If you lose it, you'll need
|
||||||
|
to create a new one.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
{:else if !apiKey && data.apiKey && apiKeyId}
|
||||||
|
<div class="space-y-4">
|
||||||
|
<Codeblock text={`${data.apiKey.prefix}${'x'.repeat(64)}`} displayOnly={true} />
|
||||||
|
<p class="text-muted-foreground text-xs">
|
||||||
|
For security reasons, the full API key is only shown once upon creation. If you've
|
||||||
|
lost your key, you'll need to regenerate it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Button onclick={createKey} disabled={loading}>
|
||||||
|
<Key class="h-4 w-4" />
|
||||||
|
Create API Key
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Documentation -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>API Documentation</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<!-- Authentication -->
|
||||||
|
<Collapsible.Root bind:open={authOpen}>
|
||||||
|
<Collapsible.Trigger class="flex w-full items-center justify-between rounded-lg border p-4 hover:bg-muted/50">
|
||||||
|
<h3 class="text-lg font-semibold">Authentication</h3>
|
||||||
|
{#if authOpen}
|
||||||
|
<ChevronDown class="h-4 w-4" />
|
||||||
|
{:else}
|
||||||
|
<ChevronRight class="h-4 w-4" />
|
||||||
|
{/if}
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content class="space-y-3 p-4">
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
Include your API key in the Authorization header for all requests:
|
||||||
|
</p>
|
||||||
|
<Codeblock
|
||||||
|
text={`Authorization: Bearer ${data.apiKey?.prefix ?? 'rgpl_'}your_api_key`}
|
||||||
|
displayOnly={true}
|
||||||
|
/>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
|
||||||
|
<!-- Top Coins Endpoint -->
|
||||||
|
<Collapsible.Root bind:open={topCoinsOpen}>
|
||||||
|
<Collapsible.Trigger class="flex w-full items-center justify-between rounded-lg border p-4 hover:bg-muted/50">
|
||||||
|
<div class="text-left">
|
||||||
|
<h3 class="text-lg font-semibold">Get Top Coins</h3>
|
||||||
|
<p class="text-muted-foreground text-sm">GET /api/v1/top</p>
|
||||||
|
</div>
|
||||||
|
{#if topCoinsOpen}
|
||||||
|
<ChevronDown class="h-4 w-4" />
|
||||||
|
{:else}
|
||||||
|
<ChevronRight class="h-4 w-4" />
|
||||||
|
{/if}
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content class="space-y-3 p-4">
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
Returns the top 50 coins by market cap.
|
||||||
|
</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Endpoint</h4>
|
||||||
|
<Codeblock text="GET https://rugplay.com/api/v1/top" displayOnly={true} />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Example Response</h4>
|
||||||
|
<Codeblock
|
||||||
|
text={`{
|
||||||
|
"coins": [
|
||||||
|
{
|
||||||
|
"symbol": "TEST",
|
||||||
|
"name": "Test",
|
||||||
|
"icon": "coins/test.webp",
|
||||||
|
"price": 76.52377103,
|
||||||
|
"change24h": 7652377003.1039,
|
||||||
|
"marketCap": 76523771031.04,
|
||||||
|
"volume24h": 13744958.18
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`}
|
||||||
|
displayOnly={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
|
||||||
|
<!-- Market Data Endpoint -->
|
||||||
|
<Collapsible.Root bind:open={marketDataOpen}>
|
||||||
|
<Collapsible.Trigger class="flex w-full items-center justify-between rounded-lg border p-4 hover:bg-muted/50">
|
||||||
|
<div class="text-left">
|
||||||
|
<h3 class="text-lg font-semibold">Get Market Data</h3>
|
||||||
|
<p class="text-muted-foreground text-sm">GET /api/v1/market</p>
|
||||||
|
</div>
|
||||||
|
{#if marketDataOpen}
|
||||||
|
<ChevronDown class="h-4 w-4" />
|
||||||
|
{:else}
|
||||||
|
<ChevronRight class="h-4 w-4" />
|
||||||
|
{/if}
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content class="space-y-3 p-4">
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
Returns paginated market data with filtering and sorting options.
|
||||||
|
</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Endpoint</h4>
|
||||||
|
<Codeblock text="GET https://rugplay.com/api/v1/market" displayOnly={true} />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Query Parameters</h4>
|
||||||
|
<div class="rounded-md border p-3">
|
||||||
|
<div class="space-y-1 text-sm">
|
||||||
|
<div><code class="bg-muted rounded px-1">search</code> - Search by coin name or symbol</div>
|
||||||
|
<div><code class="bg-muted rounded px-1">sortBy</code> - Sort field: marketCap, currentPrice, change24h, volume24h, createdAt (default: marketCap)</div>
|
||||||
|
<div><code class="bg-muted rounded px-1">sortOrder</code> - Sort order: asc, desc (default: desc)</div>
|
||||||
|
<div><code class="bg-muted rounded px-1">priceFilter</code> - Price range: all, under1, 1to10, 10to100, over100 (default: all)</div>
|
||||||
|
<div><code class="bg-muted rounded px-1">changeFilter</code> - Change filter: all, gainers, losers, hot, wild (default: all)</div>
|
||||||
|
<div><code class="bg-muted rounded px-1">page</code> - Page number (default: 1)</div>
|
||||||
|
<div><code class="bg-muted rounded px-1">limit</code> - Items per page, max 100 (default: 12)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Example Response</h4>
|
||||||
|
<Codeblock
|
||||||
|
text={`{
|
||||||
|
"coins": [
|
||||||
|
{
|
||||||
|
"symbol": "TEST",
|
||||||
|
"name": "Test",
|
||||||
|
"icon": "coins/test.webp",
|
||||||
|
"currentPrice": 76.52377103,
|
||||||
|
"marketCap": 76523771031.04,
|
||||||
|
"volume24h": 13744958.18,
|
||||||
|
"change24h": 7652377003.1039,
|
||||||
|
"createdAt": "2025-06-24T16:18:51.278Z",
|
||||||
|
"creatorName": "FaceDev"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 150,
|
||||||
|
"page": 1,
|
||||||
|
"limit": 12,
|
||||||
|
"totalPages": 13
|
||||||
|
}`}
|
||||||
|
displayOnly={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
|
||||||
|
<!-- Coin Details Endpoint -->
|
||||||
|
<Collapsible.Root bind:open={coinDetailsOpen}>
|
||||||
|
<Collapsible.Trigger class="flex w-full items-center justify-between rounded-lg border p-4 hover:bg-muted/50">
|
||||||
|
<div class="text-left">
|
||||||
|
<h3 class="text-lg font-semibold">Get Coin Details</h3>
|
||||||
|
<p class="text-muted-foreground text-sm">GET /api/v1/coin/{symbol}</p>
|
||||||
|
</div>
|
||||||
|
{#if coinDetailsOpen}
|
||||||
|
<ChevronDown class="h-4 w-4" />
|
||||||
|
{:else}
|
||||||
|
<ChevronRight class="h-4 w-4" />
|
||||||
|
{/if}
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content class="space-y-3 p-4">
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
Returns detailed information about a specific coin including price history.
|
||||||
|
</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Endpoint</h4>
|
||||||
|
<Codeblock text="GET https://rugplay.com/api/v1/coin/{symbol}" displayOnly={true} />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Parameters</h4>
|
||||||
|
<div class="rounded-md border p-3">
|
||||||
|
<div class="space-y-1 text-sm">
|
||||||
|
<div><code class="bg-muted rounded px-1">symbol</code> - Coin symbol (e.g., "TEST")</div>
|
||||||
|
<div><code class="bg-muted rounded px-1">timeframe</code> - Optional. Chart timeframe: 1m, 5m, 15m, 1h, 4h, 1d (default: 1m)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Example Response</h4>
|
||||||
|
<Codeblock
|
||||||
|
text={`{
|
||||||
|
"coin": {
|
||||||
|
"id": 2668,
|
||||||
|
"name": "Test",
|
||||||
|
"symbol": "TEST",
|
||||||
|
"icon": "coins/test.webp",
|
||||||
|
"currentPrice": 76.70938996,
|
||||||
|
"marketCap": 76709389959.04,
|
||||||
|
"volume24h": 13764558.38,
|
||||||
|
"change24h": 7670938895.9045,
|
||||||
|
"poolCoinAmount": 114176.23963001,
|
||||||
|
"poolBaseCurrencyAmount": 8758389.68983547,
|
||||||
|
"circulatingSupply": 1000000000,
|
||||||
|
"initialSupply": 1000000000,
|
||||||
|
"isListed": true,
|
||||||
|
"createdAt": "2025-06-24T16:18:51.278Z",
|
||||||
|
"creatorId": 1,
|
||||||
|
"creatorName": "FaceDev",
|
||||||
|
"creatorUsername": "facedev",
|
||||||
|
"creatorBio": "the one and only",
|
||||||
|
"creatorImage": "avatars/1.jpg"
|
||||||
|
},
|
||||||
|
"candlestickData": [
|
||||||
|
{
|
||||||
|
"time": 1750805760,
|
||||||
|
"open": 74.96948181,
|
||||||
|
"high": 74.96948181,
|
||||||
|
"low": 74.96948181,
|
||||||
|
"close": 74.96948181
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"volumeData": [
|
||||||
|
{
|
||||||
|
"time": 1750805760,
|
||||||
|
"volume": 1234.56
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeframe": "1m"
|
||||||
|
}`}
|
||||||
|
displayOnly={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
|
||||||
|
<!-- Coin Holders Endpoint -->
|
||||||
|
<Collapsible.Root bind:open={holdersOpen}>
|
||||||
|
<Collapsible.Trigger class="flex w-full items-center justify-between rounded-lg border p-4 hover:bg-muted/50">
|
||||||
|
<div class="text-left">
|
||||||
|
<h3 class="text-lg font-semibold">Get Coin Holders</h3>
|
||||||
|
<p class="text-muted-foreground text-sm">GET /api/v1/holders/{symbol}</p>
|
||||||
|
</div>
|
||||||
|
{#if holdersOpen}
|
||||||
|
<ChevronDown class="h-4 w-4" />
|
||||||
|
{:else}
|
||||||
|
<ChevronRight class="h-4 w-4" />
|
||||||
|
{/if}
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content class="space-y-3 p-4">
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
Returns the top 50 holders of a specific coin.
|
||||||
|
</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Endpoint</h4>
|
||||||
|
<Codeblock text="GET https://rugplay.com/api/v1/holders/{symbol}" displayOnly={true} />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Parameters</h4>
|
||||||
|
<div class="rounded-md border p-3">
|
||||||
|
<div class="space-y-1 text-sm">
|
||||||
|
<div><code class="bg-muted rounded px-1">symbol</code> - Coin symbol (e.g., "TEST")</div>
|
||||||
|
<div><code class="bg-muted rounded px-1">limit</code> - Number of holders to return, max 200 (default: 50)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Example Response</h4>
|
||||||
|
<Codeblock
|
||||||
|
text={`{
|
||||||
|
"coinSymbol": "TEST",
|
||||||
|
"totalHolders": 50,
|
||||||
|
"circulatingSupply": 1000000000,
|
||||||
|
"poolInfo": {
|
||||||
|
"coinAmount": 114176.23963001,
|
||||||
|
"baseCurrencyAmount": 8758389.68983547,
|
||||||
|
"currentPrice": 76.70938996
|
||||||
|
},
|
||||||
|
"holders": [
|
||||||
|
{
|
||||||
|
"rank": 1,
|
||||||
|
"userId": 1,
|
||||||
|
"username": "facedev",
|
||||||
|
"name": "FaceDev",
|
||||||
|
"image": "avatars/1.jpg",
|
||||||
|
"quantity": 999883146.4679264,
|
||||||
|
"percentage": 99.98831464679265,
|
||||||
|
"liquidationValue": 4368219.41924125
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`}
|
||||||
|
displayOnly={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
|
||||||
|
<!-- Hopium Questions Endpoint -->
|
||||||
|
<Collapsible.Root bind:open={hopiumOpen}>
|
||||||
|
<Collapsible.Trigger class="flex w-full items-center justify-between rounded-lg border p-4 hover:bg-muted/50">
|
||||||
|
<div class="text-left">
|
||||||
|
<h3 class="text-lg font-semibold">Get Prediction Markets (Hopium)</h3>
|
||||||
|
<p class="text-muted-foreground text-sm">GET /api/v1/hopium</p>
|
||||||
|
</div>
|
||||||
|
{#if hopiumOpen}
|
||||||
|
<ChevronDown class="h-4 w-4" />
|
||||||
|
{:else}
|
||||||
|
<ChevronRight class="h-4 w-4" />
|
||||||
|
{/if}
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content class="space-y-3 p-4">
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
Returns prediction market questions with pagination and filtering options.
|
||||||
|
</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Endpoint</h4>
|
||||||
|
<Codeblock text="GET https://rugplay.com/api/v1/hopium" displayOnly={true} />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Query Parameters</h4>
|
||||||
|
<div class="rounded-md border p-3">
|
||||||
|
<div class="space-y-1 text-sm">
|
||||||
|
<div><code class="bg-muted rounded px-1">status</code> - Filter by status: ACTIVE, RESOLVED, CANCELLED, ALL (default: ACTIVE)</div>
|
||||||
|
<div><code class="bg-muted rounded px-1">page</code> - Page number (default: 1)</div>
|
||||||
|
<div><code class="bg-muted rounded px-1">limit</code> - Items per page, max 100 (default: 20)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Example Response</h4>
|
||||||
|
<Codeblock
|
||||||
|
text={`{
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"question": "will elon musk tweet about rugplay?",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"resolutionDate": "2025-07-25T10:39:19.612Z",
|
||||||
|
"totalAmount": 4007.76,
|
||||||
|
"yesAmount": 3634.65,
|
||||||
|
"noAmount": 373.11,
|
||||||
|
"yesPercentage": 90.69,
|
||||||
|
"noPercentage": 9.31,
|
||||||
|
"createdAt": "2025-06-25T10:39:19.613Z",
|
||||||
|
"resolvedAt": null,
|
||||||
|
"requiresWebSearch": true,
|
||||||
|
"aiResolution": null,
|
||||||
|
"creator": {
|
||||||
|
"id": 3873,
|
||||||
|
"name": "Eliaz",
|
||||||
|
"username": "eluskulus",
|
||||||
|
"image": "avatars/102644133851219200932.png"
|
||||||
|
},
|
||||||
|
"userBets": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 150,
|
||||||
|
"page": 1,
|
||||||
|
"limit": 20,
|
||||||
|
"totalPages": 8
|
||||||
|
}`}
|
||||||
|
displayOnly={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
|
||||||
|
<!-- Hopium Question Details Endpoint -->
|
||||||
|
<Collapsible.Root bind:open={hopiumDetailsOpen}>
|
||||||
|
<Collapsible.Trigger class="flex w-full items-center justify-between rounded-lg border p-4 hover:bg-muted/50">
|
||||||
|
<div class="text-left">
|
||||||
|
<h3 class="text-lg font-semibold">Get Prediction Market Details</h3>
|
||||||
|
<p class="text-muted-foreground text-sm">GET /api/v1/hopium/{question_id}</p>
|
||||||
|
</div>
|
||||||
|
{#if hopiumDetailsOpen}
|
||||||
|
<ChevronDown class="h-4 w-4" />
|
||||||
|
{:else}
|
||||||
|
<ChevronRight class="h-4 w-4" />
|
||||||
|
{/if}
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content class="space-y-3 p-4">
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
Returns detailed information about a specific prediction market question including recent bets and probability history.
|
||||||
|
</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Endpoint</h4>
|
||||||
|
<Codeblock text="GET https://rugplay.com/api/v1/hopium/{question_id}" displayOnly={true} />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Parameters</h4>
|
||||||
|
<div class="rounded-md border p-3">
|
||||||
|
<div class="space-y-1 text-sm">
|
||||||
|
<div><code class="bg-muted rounded px-1">question_id</code> - Question ID (e.g., 101)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">Example Response</h4>
|
||||||
|
<Codeblock
|
||||||
|
text={`{
|
||||||
|
"question": {
|
||||||
|
"id": 101,
|
||||||
|
"question": "will elon musk tweet about rugplay?",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"resolutionDate": "2025-07-25T10:39:19.612Z",
|
||||||
|
"totalAmount": 4007.76,
|
||||||
|
"yesAmount": 3634.65,
|
||||||
|
"noAmount": 373.11,
|
||||||
|
"yesPercentage": 90.69,
|
||||||
|
"noPercentage": 9.31,
|
||||||
|
"createdAt": "2025-06-25T10:39:19.613Z",
|
||||||
|
"resolvedAt": null,
|
||||||
|
"requiresWebSearch": true,
|
||||||
|
"aiResolution": null,
|
||||||
|
"creator": {
|
||||||
|
"id": 3873,
|
||||||
|
"name": "Eliaz",
|
||||||
|
"username": "eluskulus",
|
||||||
|
"image": "avatars/102644133851219200932.png"
|
||||||
|
},
|
||||||
|
"userBets": null,
|
||||||
|
"recentBets": [
|
||||||
|
{
|
||||||
|
"id": 8066,
|
||||||
|
"side": true,
|
||||||
|
"amount": 3.84,
|
||||||
|
"createdAt": "2025-06-25T14:59:54.201Z",
|
||||||
|
"user": {
|
||||||
|
"id": 5332,
|
||||||
|
"name": "Spam email inhaler",
|
||||||
|
"username": "sunny_tiger7616",
|
||||||
|
"image": "avatars/111376429189149628011.webp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"probabilityHistory": [
|
||||||
|
{
|
||||||
|
"time": 1750805760,
|
||||||
|
"value": 50.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"time": 1750805820,
|
||||||
|
"value": 65.2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`}
|
||||||
|
displayOnly={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
|
||||||
|
<!-- Rate Limiting -->
|
||||||
|
<Collapsible.Root bind:open={rateLimitingOpen}>
|
||||||
|
<Collapsible.Trigger class="flex w-full items-center justify-between rounded-lg border p-4 hover:bg-muted/50">
|
||||||
|
<h3 class="text-lg font-semibold">Rate Limiting</h3>
|
||||||
|
{#if rateLimitingOpen}
|
||||||
|
<ChevronDown class="h-4 w-4" />
|
||||||
|
{:else}
|
||||||
|
<ChevronRight class="h-4 w-4" />
|
||||||
|
{/if}
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content class="p-4">
|
||||||
|
<div class="rounded-md border p-4">
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
|
<div>• <strong>Daily limit:</strong> {maxDailyRequests.toLocaleString()} requests per day</div>
|
||||||
|
<div>• <strong>Cost:</strong> 1 credit per API call</div>
|
||||||
|
<div>• <strong>Error response:</strong> 429 Too Many Requests when limit exceeded</div>
|
||||||
|
<div>• <strong>Reset:</strong> Daily limits reset every 24 hours</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
|
||||||
|
<!-- Error Responses -->
|
||||||
|
<Collapsible.Root bind:open={errorResponsesOpen}>
|
||||||
|
<Collapsible.Trigger class="flex w-full items-center justify-between rounded-lg border p-4 hover:bg-muted/50">
|
||||||
|
<h3 class="text-lg font-semibold">Error Responses</h3>
|
||||||
|
{#if errorResponsesOpen}
|
||||||
|
<ChevronDown class="h-4 w-4" />
|
||||||
|
{:else}
|
||||||
|
<ChevronRight class="h-4 w-4" />
|
||||||
|
{/if}
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content class="space-y-2 p-4">
|
||||||
|
<h4 class="font-medium">Common Error Codes</h4>
|
||||||
|
<div class="rounded-md border p-4">
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
|
<div>• <code class="bg-muted rounded px-1">400</code> - Bad Request (invalid parameters)</div>
|
||||||
|
<div>• <code class="bg-muted rounded px-1">401</code> - Unauthorized (invalid or missing API key)</div>
|
||||||
|
<div>• <code class="bg-muted rounded px-1">404</code> - Not Found (coin/question doesn't exist)</div>
|
||||||
|
<div>• <code class="bg-muted rounded px-1">429</code> - Too Many Requests (rate limit exceeded)</div>
|
||||||
|
<div>• <code class="bg-muted rounded px-1">500</code> - Internal Server Error</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
51
website/src/routes/api/keys/+server.ts
Normal file
51
website/src/routes/api/keys/+server.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import { auth } from '$lib/auth';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async (event) => {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: event.request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
throw error(401, 'Not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = await auth.api.listApiKeys({
|
||||||
|
headers: event.request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
return json(keys);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async (event) => {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: event.request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
throw error(401, 'Not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingKeys = await auth.api.listApiKeys({
|
||||||
|
headers: event.request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingKeys.length > 0) {
|
||||||
|
throw error(400, 'You can only have one API key at a time');
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = await auth.api.createApiKey({
|
||||||
|
body: {
|
||||||
|
name: "API Key",
|
||||||
|
remaining: 2000,
|
||||||
|
permissions: {
|
||||||
|
api: ['read']
|
||||||
|
},
|
||||||
|
userId: session.user.id
|
||||||
|
},
|
||||||
|
headers: event.request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
return json(apiKey);
|
||||||
|
};
|
||||||
49
website/src/routes/api/keys/[id]/+server.ts
Normal file
49
website/src/routes/api/keys/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import { auth } from '$lib/auth';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async (event) => {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: event.request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
throw error(401, 'Not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyId = event.params.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const key = await auth.api.getApiKey({
|
||||||
|
query: { id: keyId },
|
||||||
|
headers: event.request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
throw error(404, 'API key not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(key);
|
||||||
|
} catch (err) {
|
||||||
|
throw error(404, 'API key not found');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DELETE: RequestHandler = async (event) => {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: event.request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
throw error(401, 'Not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyId = event.params.id;
|
||||||
|
|
||||||
|
await auth.api.deleteApiKey({
|
||||||
|
body: { keyId },
|
||||||
|
headers: event.request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
return json({ success: true });
|
||||||
|
};
|
||||||
62
website/src/routes/api/keys/[id]/regenerate/+server.ts
Normal file
62
website/src/routes/api/keys/[id]/regenerate/+server.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import { auth } from '$lib/auth';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async (event) => {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: event.request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
throw error(401, 'Not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingKey = await auth.api.getApiKey({
|
||||||
|
query: { id: event.params.id },
|
||||||
|
headers: event.request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingKey) {
|
||||||
|
throw error(404, 'API key not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingKey.userId !== session.user.id) {
|
||||||
|
throw error(403, 'Not authorized to regenerate this API key');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(existingKey.remaining)
|
||||||
|
await auth.api.deleteApiKey({
|
||||||
|
body: { keyId: event.params.id },
|
||||||
|
headers: event.request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
let parsedPermissions: Record<string, string[]> | undefined = existingKey.permissions as Record<string, string[]> | undefined;
|
||||||
|
if (typeof existingKey.permissions === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(existingKey.permissions);
|
||||||
|
parsedPermissions = parsed && typeof parsed === 'object' ? parsed : undefined;
|
||||||
|
} catch {
|
||||||
|
parsedPermissions = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newKey = await auth.api.createApiKey({
|
||||||
|
body: {
|
||||||
|
name: existingKey.name ?? undefined,
|
||||||
|
userId: existingKey.userId,
|
||||||
|
remaining: existingKey.remaining,
|
||||||
|
refillAmount: existingKey.refillAmount ?? undefined,
|
||||||
|
refillInterval: existingKey.refillInterval ?? undefined,
|
||||||
|
rateLimitEnabled: existingKey.rateLimitEnabled,
|
||||||
|
rateLimitTimeWindow: existingKey.rateLimitTimeWindow ?? undefined,
|
||||||
|
rateLimitMax: existingKey.rateLimitMax ?? undefined,
|
||||||
|
permissions: parsedPermissions,
|
||||||
|
metadata: existingKey.metadata
|
||||||
|
},
|
||||||
|
headers: event.request.headers
|
||||||
|
});
|
||||||
|
console.log(existingKey.remaining)
|
||||||
|
console.log(newKey.remaining)
|
||||||
|
|
||||||
|
return json(newKey);
|
||||||
|
};
|
||||||
8
website/src/routes/api/v1/coin/[coinSymbol]/+server.ts
Normal file
8
website/src/routes/api/v1/coin/[coinSymbol]/+server.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { GET as getCoinData } from '../../../coin/[coinSymbol]/+server';
|
||||||
|
import { verifyApiKeyAndGetUser } from '$lib/server/api-auth';
|
||||||
|
|
||||||
|
export async function GET({ params, url, request }) {
|
||||||
|
await verifyApiKeyAndGetUser(request);
|
||||||
|
|
||||||
|
return await getCoinData({ params, url });
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { GET as getHoldersData } from '../../../coin/[coinSymbol]/holders/+server';
|
||||||
|
import { verifyApiKeyAndGetUser } from '$lib/server/api-auth';
|
||||||
|
|
||||||
|
export async function GET({ params, url, request }) {
|
||||||
|
await verifyApiKeyAndGetUser(request);
|
||||||
|
|
||||||
|
return await getHoldersData({ params, url });
|
||||||
|
}
|
||||||
19
website/src/routes/api/v1/hopium/+server.ts
Normal file
19
website/src/routes/api/v1/hopium/+server.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { GET as getHopiumQuestions } from '../../hopium/questions/+server';
|
||||||
|
import { verifyApiKeyAndGetUser } from '$lib/server/api-auth';
|
||||||
|
|
||||||
|
export async function GET({ url, request }) {
|
||||||
|
await verifyApiKeyAndGetUser(request);
|
||||||
|
|
||||||
|
const hopiumEvent = {
|
||||||
|
request,
|
||||||
|
url,
|
||||||
|
cookies: arguments[0].cookies,
|
||||||
|
fetch: arguments[0].fetch,
|
||||||
|
getClientAddress: arguments[0].getClientAddress,
|
||||||
|
locals: arguments[0].locals,
|
||||||
|
platform: arguments[0].platform,
|
||||||
|
route: { id: "/api/hopium/questions" }
|
||||||
|
};
|
||||||
|
|
||||||
|
return await getHopiumQuestions(hopiumEvent as any);
|
||||||
|
}
|
||||||
20
website/src/routes/api/v1/hopium/[id]/+server.ts
Normal file
20
website/src/routes/api/v1/hopium/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { GET as getHopiumQuestion } from '../../../hopium/questions/[id]/+server';
|
||||||
|
import { verifyApiKeyAndGetUser } from '$lib/server/api-auth';
|
||||||
|
|
||||||
|
export async function GET(event) {
|
||||||
|
await verifyApiKeyAndGetUser(event.request);
|
||||||
|
|
||||||
|
const hopiumEvent = {
|
||||||
|
params: event.params,
|
||||||
|
request: event.request,
|
||||||
|
url: event.url,
|
||||||
|
cookies: event.cookies,
|
||||||
|
fetch: event.fetch,
|
||||||
|
getClientAddress: event.getClientAddress,
|
||||||
|
locals: event.locals,
|
||||||
|
platform: event.platform,
|
||||||
|
route: { id: "/api/hopium/questions/[id]" }
|
||||||
|
};
|
||||||
|
|
||||||
|
return await getHopiumQuestion(hopiumEvent);
|
||||||
|
}
|
||||||
7
website/src/routes/api/v1/market/+server.ts
Normal file
7
website/src/routes/api/v1/market/+server.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { verifyApiKeyAndGetUser } from '$lib/server/api-auth';
|
||||||
|
import { GET as getMarketData } from '../../market/+server';
|
||||||
|
|
||||||
|
export async function GET({ url, request }) {
|
||||||
|
await verifyApiKeyAndGetUser(request);
|
||||||
|
return await getMarketData({ url });
|
||||||
|
}
|
||||||
7
website/src/routes/api/v1/top/+server.ts
Normal file
7
website/src/routes/api/v1/top/+server.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { verifyApiKeyAndGetUser } from '$lib/server/api-auth';
|
||||||
|
import { GET as getTopCoins } from '../../coins/top/+server';
|
||||||
|
|
||||||
|
export async function GET({ request }) {
|
||||||
|
await verifyApiKeyAndGetUser(request);
|
||||||
|
return await getTopCoins();
|
||||||
|
}
|
||||||
Reference in a new issue