feat: api
This commit is contained in:
parent
ee29f97ca4
commit
45a49e3f2f
29 changed files with 1622 additions and 5532 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
@ -411,7 +417,7 @@
|
|||
</div>
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
|
||||
<!-- Profile & Settings Group -->
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item onclick={handleAccountClick}>
|
||||
|
|
@ -427,11 +433,17 @@
|
|||
Prestige
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
|
||||
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
|
||||
<!-- Features Group -->
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item
|
||||
onclick={handleAPIClick}
|
||||
>
|
||||
<Key />
|
||||
API
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
showPromoCode = true;
|
||||
|
|
@ -483,9 +495,9 @@
|
|||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
{/if}
|
||||
|
||||
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
|
||||
<!-- Legal Group -->
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item onclick={handleTermsClick}>
|
||||
|
|
@ -497,9 +509,9 @@
|
|||
Privacy Policy
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
|
||||
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
|
||||
<!-- Sign Out -->
|
||||
<DropdownMenu.Item
|
||||
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),
|
||||
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