fix: API key test now actually verifies the key against a real auth endpoint
The old test hit /api/games/lookup which is a public endpoint with no auth, so it never checked the key — any key (or a deleted one) reported as valid. - Add GET /api/plugin/verify-key endpoint that uses authenticateWithApiKey to properly verify the x-api-key header via Better Auth - Plugin test_api_key now calls /api/plugin/verify-key instead of games-lookup - Returns the linked user's name + avatar on success for richer feedback
This commit is contained in:
@@ -4,6 +4,7 @@ import { db } from "@/lib/db/index"
|
|||||||
import { pluginPairing } from "@/lib/db/schema"
|
import { pluginPairing } from "@/lib/db/schema"
|
||||||
import { eq, and, lt } from "drizzle-orm"
|
import { eq, and, lt } from "drizzle-orm"
|
||||||
import { auth } from "@/lib/auth"
|
import { auth } from "@/lib/auth"
|
||||||
|
import { authenticateWithApiKey } from "@/lib/auth/api-key-guard"
|
||||||
|
|
||||||
const PAIRING_TTL_MS = 10 * 60 * 1000 // 10 minutes
|
const PAIRING_TTL_MS = 10 * 60 * 1000 // 10 minutes
|
||||||
|
|
||||||
@@ -26,6 +27,28 @@ export const pluginPairingRoutes = new Elysia({
|
|||||||
prefix: "/plugin",
|
prefix: "/plugin",
|
||||||
detail: { tags: ["Plugin"] },
|
detail: { tags: ["Plugin"] },
|
||||||
})
|
})
|
||||||
|
// ── Verify API key: plugin checks if its key is still valid ──
|
||||||
|
.get(
|
||||||
|
"/verify-key",
|
||||||
|
async ({ request, set }) => {
|
||||||
|
const guard = await authenticateWithApiKey(request.headers)
|
||||||
|
if (!guard.ok) {
|
||||||
|
set.status = guard.status
|
||||||
|
return { valid: false, error: guard.error }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
user: { name: guard.user.name, image: guard.user.image },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
summary: "Verify a Decky plugin API key",
|
||||||
|
description:
|
||||||
|
"Checks whether the x-api-key header contains a valid, enabled API key. Used by the plugin to test its saved key.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
// ── Initiate: plugin requests a pairing token (no auth) ──────
|
// ── Initiate: plugin requests a pairing token (no auth) ──────
|
||||||
.post(
|
.post(
|
||||||
"/pair/initiate",
|
"/pair/initiate",
|
||||||
|
|||||||
@@ -599,31 +599,40 @@ exec mangohud "$@"
|
|||||||
return {"success": False, "error": str(e), "status": 0}
|
return {"success": False, "error": str(e), "status": 0}
|
||||||
|
|
||||||
async def test_api_key(self, api_key: str, base_url: str = "https://deckyvault.xyz") -> dict:
|
async def test_api_key(self, api_key: str, base_url: str = "https://deckyvault.xyz") -> dict:
|
||||||
"""RPC: Test if an API key is valid by calling the games lookup endpoint.
|
"""RPC: Test if an API key is valid by calling the dedicated verify endpoint.
|
||||||
Returns {valid: bool, error: str?}."""
|
Returns {valid: bool, error: str?, userName?: str, userImage?: str}."""
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.error
|
import urllib.error
|
||||||
|
|
||||||
|
if not api_key:
|
||||||
|
return {"valid": False, "error": "No API key provided"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
url = f"{base_url}/api/games/lookup?steamAppId=0"
|
url = f"{base_url}/api/plugin/verify-key"
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
url,
|
url,
|
||||||
headers={
|
headers={
|
||||||
"x-api-key": api_key,
|
"x-api-key": api_key,
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:136.0) Gecko/20100101 Firefox/136.0",
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:136.0) Gecko/20100101 Firefox/136.0",
|
||||||
|
"Accept": "application/json",
|
||||||
},
|
},
|
||||||
method="GET"
|
method="GET"
|
||||||
)
|
)
|
||||||
context = _get_ssl_context()
|
context = _get_ssl_context()
|
||||||
with urllib.request.urlopen(req, timeout=10, context=context) as response:
|
with urllib.request.urlopen(req, timeout=10, context=context) as response:
|
||||||
# A 404 (game not found) still means the API key is valid
|
result = json.loads(response.read().decode('utf-8'))
|
||||||
return {"valid": True}
|
return {
|
||||||
|
"valid": True,
|
||||||
|
"userName": result.get("user", {}).get("name"),
|
||||||
|
"userImage": result.get("user", {}).get("image"),
|
||||||
|
}
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
if e.code == 401:
|
if e.code == 401:
|
||||||
return {"valid": False, "error": "Invalid API key"}
|
return {"valid": False, "error": "Invalid or revoked API key"}
|
||||||
elif e.code in (400, 404):
|
try:
|
||||||
return {"valid": True} # Key works, just bad request or no game with ID 0
|
err = json.loads(e.read().decode('utf-8'))
|
||||||
else:
|
return {"valid": False, "error": err.get("error", f"Server returned status {e.code}")}
|
||||||
|
except Exception:
|
||||||
return {"valid": False, "error": f"Server returned status {e.code}"}
|
return {"valid": False, "error": f"Server returned status {e.code}"}
|
||||||
except urllib.error.URLError as e:
|
except urllib.error.URLError as e:
|
||||||
return {"valid": False, "error": f"Network error: {str(e.reason)}"}
|
return {"valid": False, "error": f"Network error: {str(e.reason)}"}
|
||||||
|
|||||||
Reference in New Issue
Block a user