feat: api

This commit is contained in:
Face 2025-06-25 22:33:07 +03:00
parent ee29f97ca4
commit 45a49e3f2f
29 changed files with 1622 additions and 5532 deletions

View file

@ -42,6 +42,30 @@ CREATE TABLE IF NOT EXISTS "account_deletion_request" (
CONSTRAINT "account_deletion_request_user_id_unique" UNIQUE("user_id")
);
--> 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" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar(255) NOT NULL,
@ -189,6 +213,7 @@ CREATE TABLE IF NOT EXISTS "user" (
"last_reward_claim" timestamp with time zone,
"total_rewards_claimed" numeric(20, 8) DEFAULT '0.00000000' NOT NULL,
"login_streak" integer DEFAULT 0 NOT NULL,
"prestige_level" integer DEFAULT 0,
CONSTRAINT "user_email_unique" UNIQUE("email"),
CONSTRAINT "user_username_unique" UNIQUE("username")
);
@ -222,6 +247,12 @@ EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> 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
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
@ -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_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 "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_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
@ -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_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_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");

View file

@ -1 +0,0 @@
CREATE INDEX IF NOT EXISTS "prediction_question_status_resolution_idx" ON "prediction_question" USING btree ("status","resolution_date");

View file

@ -1 +0,0 @@
ALTER TABLE "user" ADD COLUMN "prestige_level" integer DEFAULT 0;

View file

@ -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");

View file

@ -1,5 +1,5 @@
{
"id": "41f7bba3-1d5d-41ba-83bb-ca129ace81f0",
"id": "08d1623b-7b2d-4777-8b7b-dbfa7884ecfe",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"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": {
"name": "coin",
"schema": "",
@ -333,7 +499,128 @@
"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": {
"coin_creator_id_user_id_fk": {
"name": "coin_creator_id_user_id_fk",
@ -955,6 +1242,27 @@
"concurrently": false,
"method": "btree",
"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": {
@ -1338,7 +1646,110 @@
"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": {
"transaction_user_id_user_id_fk": {
"name": "transaction_user_id_user_id_fk",
@ -1518,9 +1929,92 @@
"primaryKey": false,
"notNull": true,
"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": {},
"compositePrimaryKeys": {},
"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

View file

@ -5,29 +5,8 @@
{
"idx": 0,
"version": "7",
"when": 1749654046953,
"tag": "0000_crazy_bloodstrike",
"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",
"when": 1750863600119,
"tag": "0000_chief_korath",
"breakpoints": true
}
]

View file

@ -6,6 +6,7 @@ import { db } from "./server/db";
import * as schema from "./server/db/schema";
import { generateUsername } from "./utils/random";
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_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",
],
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, {
provider: "pg",
schema: schema,

View file

@ -32,7 +32,8 @@
BookOpen,
Info,
Bell,
Crown
Crown,
Key
} from 'lucide-svelte';
import { mode, setMode } from 'mode-watcher';
import type { HTMLAttributes } from 'svelte/elements';
@ -158,6 +159,11 @@
goto('/prestige');
setOpenMobile(false);
}
function handleAPIClick(){
goto('/api');
setOpenMobile(false);
}
</script>
<SignInConfirmDialog bind:open={shouldSignIn} />
@ -432,6 +438,12 @@
<!-- Features Group -->
<DropdownMenu.Group>
<DropdownMenu.Item
onclick={handleAPIClick}
>
<Key />
API
</DropdownMenu.Item>
<DropdownMenu.Item
onclick={() => {
showPromoCode = true;

View 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>

View file

@ -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} />

View file

@ -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} />

View 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} />

View 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,
};

View 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;
}

View file

@ -275,3 +275,29 @@ export const notifications = pgTable("notification", {
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)
}));

View 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
};
};

View 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/&lbrace;symbol&rbrace;</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/&lbrace;symbol&rbrace;" 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/&lbrace;symbol&rbrace;</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/&lbrace;symbol&rbrace;" 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/&lbrace;question_id&rbrace;</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/&lbrace;question_id&rbrace;" 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>

View 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);
};

View 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 });
};

View 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);
};

View 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 });
}

View file

@ -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 });
}

View 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);
}

View 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);
}

View 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 });
}

View 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();
}