summaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-07 05:03:35 -0800
committerFuwn <[email protected]>2026-02-07 05:03:35 -0800
commit17f0f1877764e9c1d79a2b86dac8ff2110aa6a34 (patch)
tree1cabf70d230dc614c3ee2408bdc0acaf13a502d8 /apps
parentfeat: dynamically compute sidebar max width from item text widths (diff)
downloadasa.news-17f0f1877764e9c1d79a2b86dac8ff2110aa6a34.tar.xz
asa.news-17f0f1877764e9c1d79a2b86dac8ff2110aa6a34.zip
fix: api key prefix rename, revoke fix, and webhook validation
Rename API key prefix from asn_ to asa_, fix key revoke by aligning response property names with frontend interface, and add server/client validation to prevent enabling webhooks without a URL.
Diffstat (limited to 'apps')
-rw-r--r--apps/web/app/api/v1/keys/route.ts27
-rw-r--r--apps/web/app/api/webhook-config/route.ts21
-rw-r--r--apps/web/app/reader/settings/_components/api-settings.tsx4
-rw-r--r--apps/web/lib/api-auth.ts2
-rw-r--r--apps/web/lib/api-key.ts2
5 files changed, 40 insertions, 16 deletions
diff --git a/apps/web/app/api/v1/keys/route.ts b/apps/web/app/api/v1/keys/route.ts
index 1461532..de63a46 100644
--- a/apps/web/app/api/v1/keys/route.ts
+++ b/apps/web/app/api/v1/keys/route.ts
@@ -31,14 +31,15 @@ export async function GET() {
)
}
+ const activeKeys = keys.filter((key) => key.revoked_at === null)
+
return NextResponse.json({
- keys: keys.map((key) => ({
- identifier: key.id,
+ keys: activeKeys.map((key) => ({
+ keyIdentifier: key.id,
keyPrefix: key.key_prefix,
label: key.label,
createdAt: key.created_at,
lastUsedAt: key.last_used_at,
- isRevoked: key.revoked_at !== null,
})),
})
}
@@ -94,12 +95,16 @@ export async function POST(request: Request) {
const { fullKey, keyHash, keyPrefix } = generateApiKey()
- const { error: insertError } = await adminClient.from("api_keys").insert({
- user_id: user.id,
- key_hash: keyHash,
- key_prefix: keyPrefix,
- label,
- })
+ const { data: insertedKey, error: insertError } = await adminClient
+ .from("api_keys")
+ .insert({
+ user_id: user.id,
+ key_hash: keyHash,
+ key_prefix: keyPrefix,
+ label,
+ })
+ .select("id")
+ .single()
if (insertError) {
return NextResponse.json(
@@ -109,8 +114,8 @@ export async function POST(request: Request) {
}
return NextResponse.json({
- key: fullKey,
+ fullKey,
keyPrefix,
- label,
+ keyIdentifier: insertedKey.id,
})
}
diff --git a/apps/web/app/api/webhook-config/route.ts b/apps/web/app/api/webhook-config/route.ts
index 049f4f3..bd38d0f 100644
--- a/apps/web/app/api/webhook-config/route.ts
+++ b/apps/web/app/api/webhook-config/route.ts
@@ -91,10 +91,29 @@ export async function PUT(request: Request) {
}
if (typeof body.webhookEnabled === "boolean") {
- updates.webhook_enabled = body.webhookEnabled
if (body.webhookEnabled) {
+ const { data: currentProfile } = await adminClient
+ .from("user_profiles")
+ .select("webhook_url")
+ .eq("id", user.id)
+ .single()
+
+ const effectiveUrl =
+ typeof body.webhookUrl === "string"
+ ? body.webhookUrl.trim()
+ : currentProfile?.webhook_url
+
+ if (!effectiveUrl) {
+ return NextResponse.json(
+ { error: "cannot enable webhooks without a url" },
+ { status: 400 }
+ )
+ }
+
updates.webhook_consecutive_failures = 0
}
+
+ updates.webhook_enabled = body.webhookEnabled
}
if (Object.keys(updates).length === 0) {
diff --git a/apps/web/app/reader/settings/_components/api-settings.tsx b/apps/web/app/reader/settings/_components/api-settings.tsx
index 0ae6a8d..cca673f 100644
--- a/apps/web/app/reader/settings/_components/api-settings.tsx
+++ b/apps/web/app/reader/settings/_components/api-settings.tsx
@@ -431,7 +431,7 @@ function WebhookSection() {
</button>
<button
onClick={handleToggleEnabled}
- disabled={updateWebhookConfig.isPending}
+ disabled={updateWebhookConfig.isPending || (!webhookConfig?.webhookEnabled && !webhookConfig?.webhookUrl)}
className="border border-border px-4 py-1.5 text-text-secondary transition-colors hover:text-text-primary disabled:opacity-50"
>
{webhookConfig?.webhookEnabled ? "disable" : "enable"}
@@ -511,7 +511,7 @@ export function ApiSettings() {
authenticate requests with an api key in the authorization header:
</p>
<code className="block bg-background-tertiary px-3 py-2 text-text-secondary">
- Authorization: Bearer asn_your_key_here
+ Authorization: Bearer asa_your_key_here
</code>
<div className="mt-3 space-y-1 text-text-dim">
<p>get /api/v1/profile — your account info and limits</p>
diff --git a/apps/web/lib/api-auth.ts b/apps/web/lib/api-auth.ts
index 3f819f2..d2efdd7 100644
--- a/apps/web/lib/api-auth.ts
+++ b/apps/web/lib/api-auth.ts
@@ -25,7 +25,7 @@ export async function authenticateApiRequest(
const apiKey = authorizationHeader.slice(7)
- if (!apiKey.startsWith("asn_")) {
+ if (!apiKey.startsWith("asa_")) {
return { authenticated: false, status: 401, error: "invalid api key format" }
}
diff --git a/apps/web/lib/api-key.ts b/apps/web/lib/api-key.ts
index ce59f89..f5beaac 100644
--- a/apps/web/lib/api-key.ts
+++ b/apps/web/lib/api-key.ts
@@ -1,6 +1,6 @@
import { randomBytes, createHash } from "crypto"
-const API_KEY_PREFIX = "asn_"
+const API_KEY_PREFIX = "asa_"
export function generateApiKey(): {
fullKey: string