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)
|
||||
}));
|
||||
Reference in a new issue