From da12e8de79c90ebbecebb63c17de63a2352ebfcc Mon Sep 17 00:00:00 2001 From: pluja Date: Fri, 30 May 2025 08:17:23 +0000 Subject: [PATCH] add basic API plus minor updates and fixes --- web/src/pages/about.mdx | 35 +++++++ web/src/pages/api/v1/service/[...id].ts | 121 ++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 web/src/pages/api/v1/service/[...id].ts diff --git a/web/src/pages/about.mdx b/web/src/pages/about.mdx index 53b3986..aaf5ccc 100644 --- a/web/src/pages/about.mdx +++ b/web/src/pages/about.mdx @@ -201,6 +201,41 @@ Some reviews may be spam or fake. Read comments carefully and **always do your o To **see comments waiting for moderation**, toggle the switch in the comments section. These comments show up with a yellow background and a "pending" label. +## API + +Access basic service data via our public API. + +**Attribution:** Please credit **KYCnot.me** if you use data from this API. + +### `GET /api/v1/service/[id]` + +Fetches details for a single service. + +- **`[id]`**: Can be a service ID, slug, name, or any registered URL (including .onion/.i2p). + +**Example Requests:** + +``` +/api/v1/service/bisq +/api/v1/service/https://bisq.network +``` + +**Example Response (200 OK):** + +```json +{ + "name": "Bisq", + "description": "Decentralized Bitcoin exchange network.", + "kycLevel": 0, + "categories": ["exchange"], + "serviceUrls": ["https://bisq.network/"], + "onionUrls": [], + "i2pUrls": [], + "tosUrls": ["https://bisq.network/terms-of-service/"], + "kycnotmeUrl": "https://kycnot.me/service/bisq" +} +``` + ## Support If you like this project, you can **support** it through these methods: diff --git a/web/src/pages/api/v1/service/[...id].ts b/web/src/pages/api/v1/service/[...id].ts new file mode 100644 index 0000000..c84d910 --- /dev/null +++ b/web/src/pages/api/v1/service/[...id].ts @@ -0,0 +1,121 @@ +import { prisma } from '../../../../lib/prisma' + +import type { Prisma } from '@prisma/client' +import type { APIRoute } from 'astro' + +const MAX_ID_LENGTH = 2048 + +export const GET: APIRoute = async ({ params }) => { + const { id } = params + + if (!id) { + return new Response(JSON.stringify({ error: 'ID parameter is missing' }), { + status: 400, + headers: { + 'Content-Type': 'application/json', + }, + }) + } + + if (id.length > MAX_ID_LENGTH) { + return new Response(JSON.stringify({ error: 'ID parameter is too long' }), { + status: 400, + headers: { + 'Content-Type': 'application/json', + }, + }) + } + + const orConditions: Prisma.ServiceWhereInput[] = [ + { slug: id }, + { name: id }, + { serviceUrls: { has: id } }, + { onionUrls: { has: id } }, + { i2pUrls: { has: id } }, + ] + + // Try direct ID lookup first + const numericId = parseInt(id, 10) + + if (!isNaN(numericId)) { + orConditions.push({ id: numericId }) + } + + if (id.startsWith('http://') || id.startsWith('https://')) { + let alternativeId: string + if (id.endsWith('/')) { + alternativeId = id.slice(0, -1) // Remove trailing slash + } else { + alternativeId = id + '/' // Add trailing slash + } + orConditions.push({ serviceUrls: { has: alternativeId } }) + orConditions.push({ onionUrls: { has: alternativeId } }) + orConditions.push({ i2pUrls: { has: alternativeId } }) + } else { + // For non-HTTP/S IDs, check as is (could be a direct onion/i2p address without protocol) + orConditions.push({ serviceUrls: { has: id } }) + orConditions.push({ onionUrls: { has: id } }) + orConditions.push({ i2pUrls: { has: id } }) + } + + try { + const service = await prisma.service.findFirst({ + where: { + OR: orConditions, + }, + select: { + name: true, + slug: true, + description: true, + kycLevel: true, + categories: { + select: { + name: true, + slug: true, + icon: true, + }, + }, + serviceUrls: true, + onionUrls: true, + i2pUrls: true, + tosUrls: true, + }, + }) + + if (!service) { + return new Response(JSON.stringify({ error: 'Service not found' }), { + status: 404, + headers: { + 'Content-Type': 'application/json', + }, + }) + } + + const responseData = { + name: service.name, + description: service.description, + kycLevel: service.kycLevel, + categories: service.categories.map((category) => category.slug), + serviceUrls: service.serviceUrls, + onionUrls: service.onionUrls, + i2pUrls: service.i2pUrls, + tosUrls: service.tosUrls, + kycnotmeUrl: `https://kycnot.me/service/${service.slug}`, + } + + return new Response(JSON.stringify(responseData), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }) + } catch (error) { + console.error('Error fetching service:', error) + return new Response(JSON.stringify({ error: 'Internal server error' }), { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + }) + } +}