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

@ -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} />
@ -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={() => {

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

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