Compare commits
4 Commits
release-43
...
release-47
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3afa824c18 | ||
|
|
9a68112e24 | ||
|
|
0c40d8eec5 | ||
|
|
e16c9b64ed |
@@ -24,7 +24,7 @@ export const apiServiceActions = {
|
||||
.optional(),
|
||||
url: zodUrlOptionalProtocol.optional(),
|
||||
}),
|
||||
handler: async (input) => {
|
||||
handler: async (input, context) => {
|
||||
if (!input.id && !input.slug && !input.url) {
|
||||
throw new ActionError({
|
||||
code: 'BAD_REQUEST',
|
||||
@@ -46,6 +46,9 @@ export const apiServiceActions = {
|
||||
|
||||
const service = await prisma.service.findFirst({
|
||||
where: {
|
||||
listedAt: { lte: new Date() },
|
||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
||||
|
||||
OR: [
|
||||
...(input.id ? ([{ id: input.id }] satisfies Prisma.ServiceWhereInput[]) : []),
|
||||
...(input.slug ? ([{ slug: input.slug }] satisfies Prisma.ServiceWhereInput[]) : []),
|
||||
@@ -76,10 +79,20 @@ export const apiServiceActions = {
|
||||
i2pUrls: true,
|
||||
tosUrls: true,
|
||||
referral: true,
|
||||
listedAt: true,
|
||||
verifiedAt: true,
|
||||
serviceVisibility: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!service) {
|
||||
if (
|
||||
!service ||
|
||||
(service.serviceVisibility !== 'PUBLIC' &&
|
||||
service.serviceVisibility !== 'ARCHIVED' &&
|
||||
service.serviceVisibility !== 'UNLISTED') ||
|
||||
!service.listedAt ||
|
||||
service.listedAt > new Date()
|
||||
) {
|
||||
throw new ActionError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Service not found',
|
||||
@@ -91,6 +104,7 @@ export const apiServiceActions = {
|
||||
slug: service.slug,
|
||||
name: service.name,
|
||||
description: service.description,
|
||||
serviceVisibility: service.serviceVisibility,
|
||||
verificationStatus: service.verificationStatus,
|
||||
verificationStatusInfo: pick(getVerificationStatusInfo(service.verificationStatus), [
|
||||
'value',
|
||||
@@ -99,14 +113,16 @@ export const apiServiceActions = {
|
||||
'labelShort',
|
||||
'description',
|
||||
]),
|
||||
verifiedAt: service.verifiedAt,
|
||||
kycLevel: service.kycLevel,
|
||||
kycLevelInfo: pick(getKycLevelInfo(service.kycLevel.toString()), ['value', 'name', 'description']),
|
||||
categories: service.categories,
|
||||
listedAt: service.listedAt,
|
||||
serviceUrls: [...service.serviceUrls, ...service.onionUrls, ...service.i2pUrls].map(
|
||||
(url) => url + (service.referral ?? '')
|
||||
),
|
||||
tosUrls: service.tosUrls,
|
||||
kycnotmeUrl: `https://kycnot.me/service/${service.slug}`,
|
||||
kycnotmeUrl: new URL(`/service/${service.slug}`, context.url).href,
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
Currency,
|
||||
ServiceSuggestionStatus,
|
||||
ServiceSuggestionType,
|
||||
ServiceVisibility,
|
||||
VerificationStatus,
|
||||
} from '@prisma/client'
|
||||
import { Currency } from '@prisma/client'
|
||||
import { z } from 'astro/zod'
|
||||
import { ActionError } from 'astro:actions'
|
||||
import { formatDistanceStrict } from 'date-fns'
|
||||
@@ -118,9 +112,9 @@ export const serviceSuggestionActions = {
|
||||
|
||||
const serviceSuggestion = await prisma.serviceSuggestion.create({
|
||||
data: {
|
||||
type: ServiceSuggestionType.EDIT_SERVICE,
|
||||
type: 'EDIT_SERVICE',
|
||||
notes: combinedNotes,
|
||||
status: ServiceSuggestionStatus.PENDING,
|
||||
status: 'PENDING',
|
||||
userId: context.locals.user.id,
|
||||
serviceId: service.id,
|
||||
},
|
||||
@@ -229,12 +223,12 @@ export const serviceSuggestionActions = {
|
||||
kycLevel: input.kycLevel,
|
||||
acceptedCurrencies: input.acceptedCurrencies,
|
||||
imageUrl,
|
||||
verificationStatus: VerificationStatus.COMMUNITY_CONTRIBUTED,
|
||||
verificationStatus: 'COMMUNITY_CONTRIBUTED',
|
||||
overallScore: 0,
|
||||
privacyScore: 0,
|
||||
trustScore: 0,
|
||||
listedAt: new Date(),
|
||||
serviceVisibility: ServiceVisibility.UNLISTED,
|
||||
serviceVisibility: 'UNLISTED',
|
||||
categories: {
|
||||
connect: input.categories.map((id) => ({ id })),
|
||||
},
|
||||
@@ -250,8 +244,8 @@ export const serviceSuggestionActions = {
|
||||
const serviceSuggestion = await tx.serviceSuggestion.create({
|
||||
data: {
|
||||
notes: input.notes,
|
||||
type: ServiceSuggestionType.CREATE_SERVICE,
|
||||
status: ServiceSuggestionStatus.PENDING,
|
||||
type: 'CREATE_SERVICE',
|
||||
status: 'PENDING',
|
||||
userId: context.locals.user.id,
|
||||
serviceId: service.id,
|
||||
},
|
||||
|
||||
@@ -27,6 +27,12 @@ const links = [
|
||||
icon: 'i2p',
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
href: '/docs/api',
|
||||
label: 'API',
|
||||
icon: 'ri:plug-line',
|
||||
external: false,
|
||||
},
|
||||
{
|
||||
href: '/about',
|
||||
label: 'About',
|
||||
|
||||
@@ -65,7 +65,7 @@ export const {
|
||||
value: 'ARCHIVED',
|
||||
slug: 'archived',
|
||||
label: 'Archived',
|
||||
description: 'No longer operational',
|
||||
description: 'No longer operational.',
|
||||
longDescription:
|
||||
'Archived service, no longer exists or ceased operations. Information may be outdated.',
|
||||
icon: 'ri:archive-line',
|
||||
|
||||
@@ -114,7 +114,13 @@ export class ErrorBanners {
|
||||
return result
|
||||
} catch (error) {
|
||||
this.handler(uiMessage)(error)
|
||||
return fallback as F
|
||||
return fallback as F extends never[]
|
||||
? T extends [infer _First, ...infer _Rest]
|
||||
? []
|
||||
: T extends unknown[]
|
||||
? T[number][]
|
||||
: F
|
||||
: F
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
50
web/src/lib/makeAdminApiCallInfo.ts
Normal file
50
web/src/lib/makeAdminApiCallInfo.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { Misc } from 'ts-toolbelt'
|
||||
|
||||
export async function makeAdminApiCallInfo<T extends Misc.JSON.Object>({
|
||||
method,
|
||||
path,
|
||||
input,
|
||||
baseUrl,
|
||||
}: {
|
||||
method: 'POST' | 'QUERY'
|
||||
path: `/${string}`
|
||||
input: T
|
||||
baseUrl: URL | string
|
||||
}) {
|
||||
const fullPath = new URL(`/api/v1${path}`, baseUrl).href
|
||||
|
||||
const fetchProsmise = fetch(fullPath, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(input),
|
||||
}).then((res) => {
|
||||
try {
|
||||
return res.json() as Promise<Misc.JSON.Value>
|
||||
} catch (errJson: unknown) {
|
||||
console.error(errJson)
|
||||
|
||||
try {
|
||||
return res.text()
|
||||
} catch (errText: unknown) {
|
||||
console.error(errText)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let output: Misc.JSON.Value = ''
|
||||
try {
|
||||
output = await fetchProsmise
|
||||
} catch (err: unknown) {
|
||||
console.error(err)
|
||||
output = err instanceof Error ? err.message : String(err)
|
||||
}
|
||||
|
||||
return {
|
||||
method,
|
||||
path,
|
||||
fullPath,
|
||||
input,
|
||||
output,
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
import { RELEASE_DATE, RELEASE_NUMBER } from 'astro:env/server'
|
||||
|
||||
import TimeFormatted from '../../components/TimeFormatted.astro'
|
||||
import MiniLayout from '../../layouts/MiniLayout.astro'
|
||||
import { timeAgo } from '../../lib/timeAgo'
|
||||
|
||||
const releaseDate =
|
||||
RELEASE_DATE && !isNaN(new Date(RELEASE_DATE).getTime()) ? new Date(RELEASE_DATE) : undefined
|
||||
@@ -37,7 +37,7 @@ const releaseDate =
|
||||
{
|
||||
!!releaseDate && (
|
||||
<p class="text-day-500 mt-2">
|
||||
(<TimeFormatted date={releaseDate} hourPrecision daysUntilDate={Infinity} />)
|
||||
(<time datetime={releaseDate.toISOString()}>{timeAgo.format(releaseDate, 'round')}</time>)
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { Markdown } from 'astro-remote'
|
||||
import { actions, isInputError } from 'astro:actions'
|
||||
import { Code } from 'astro:components'
|
||||
import { orderBy } from 'lodash-es'
|
||||
|
||||
import BadgeSmall from '../../../../components/BadgeSmall.astro'
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
verificationStepStatuses,
|
||||
} from '../../../../constants/verificationStepStatus'
|
||||
import BaseLayout from '../../../../layouts/BaseLayout.astro'
|
||||
import { makeAdminApiCallInfo } from '../../../../lib/makeAdminApiCallInfo'
|
||||
import { pluralize } from '../../../../lib/pluralize'
|
||||
import { prisma } from '../../../../lib/prisma'
|
||||
|
||||
@@ -181,6 +183,20 @@ const [service, categories, attributes] = await Astro.locals.banners.tryMany([
|
||||
])
|
||||
|
||||
if (!service) return Astro.rewrite('/404')
|
||||
|
||||
const apiCalls = await Astro.locals.banners.try(
|
||||
'Error fetching api calls',
|
||||
() =>
|
||||
Promise.all([
|
||||
makeAdminApiCallInfo({
|
||||
method: 'QUERY',
|
||||
path: '/service/get',
|
||||
input: { slug: service.slug },
|
||||
baseUrl: Astro.url,
|
||||
}),
|
||||
]),
|
||||
[]
|
||||
)
|
||||
---
|
||||
|
||||
<BaseLayout pageTitle={`Edit Service: ${service.name}`}>
|
||||
@@ -1100,5 +1116,19 @@ if (!service) return Astro.rewrite('/404')
|
||||
</form>
|
||||
</FormSubSection>
|
||||
</FormSection>
|
||||
|
||||
<FormSection title="API">
|
||||
{
|
||||
apiCalls.map((call) => (
|
||||
<FormSubSection title={`${call.method} ${call.path}`}>
|
||||
<p class="text-day-400 text-sm">Input:</p>
|
||||
<Code code={JSON.stringify(call.input, null, 2)} lang="json" class="rounded-lg p-4 text-xs" />
|
||||
|
||||
<p class="text-day-400 text-sm">Output:</p>
|
||||
<Code code={JSON.stringify(call.output, null, 2)} lang="json" class="rounded-lg p-4 text-xs" />
|
||||
</FormSubSection>
|
||||
))
|
||||
}
|
||||
</FormSection>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -10,6 +10,7 @@ icon: 'ri:plug-line'
|
||||
import { SOURCE_CODE_URL } from 'astro:env/server'
|
||||
import { kycLevels } from '../../constants/kycLevels'
|
||||
import { verificationStatuses } from '../../constants/verificationStatus'
|
||||
import { serviceVisibilities } from '../../constants/serviceVisibility'
|
||||
|
||||
Access basic service data via our public API.
|
||||
|
||||
@@ -41,6 +42,7 @@ type ServiceResponse = {
|
||||
slug: string
|
||||
name: string
|
||||
description: string
|
||||
serviceVisibility: 'PUBLIC' | 'ARCHIVED' | 'UNLISTED'
|
||||
verificationStatus: 'VERIFICATION_SUCCESS' | 'APPROVED' | 'COMMUNITY_CONTRIBUTED' | 'VERIFICATION_FAILED'
|
||||
verificationStatusInfo: {
|
||||
value: 'VERIFICATION_SUCCESS' | 'APPROVED' | 'COMMUNITY_CONTRIBUTED' | 'VERIFICATION_FAILED'
|
||||
@@ -49,6 +51,7 @@ type ServiceResponse = {
|
||||
labelShort: string
|
||||
description: string
|
||||
}
|
||||
verifiedAt: Date | null
|
||||
kycLevel: 0 | 1 | 2 | 3 | 4
|
||||
kycLevelInfo: {
|
||||
value: 0 | 1 | 2 | 3 | 4
|
||||
@@ -59,13 +62,14 @@ type ServiceResponse = {
|
||||
name: string
|
||||
slug: string
|
||||
}[]
|
||||
listedAt: Date
|
||||
serviceUrls: string[]
|
||||
tosUrls: string[]
|
||||
kycnotmeUrl: `https://kycnot.me/service/${service.slug}`
|
||||
}
|
||||
```
|
||||
|
||||
### KYC Levels
|
||||
#### KYC Levels
|
||||
|
||||
<ul>
|
||||
{kycLevels.map((level) => (
|
||||
@@ -75,7 +79,7 @@ type ServiceResponse = {
|
||||
))}
|
||||
</ul>
|
||||
|
||||
### Verification Status
|
||||
#### Verification Status
|
||||
|
||||
<ul>
|
||||
{verificationStatuses.map((status) => (
|
||||
@@ -85,7 +89,19 @@ type ServiceResponse = {
|
||||
))}
|
||||
</ul>
|
||||
|
||||
### Example Request
|
||||
#### Service Visibility
|
||||
|
||||
<ul>
|
||||
{serviceVisibilities.filter((visibility) => visibility.value === 'PUBLIC' || visibility.value === 'ARCHIVED' || visibility.value === 'UNLISTED').map((visibility) => (
|
||||
<li key={visibility.value}>
|
||||
<strong>{visibility.value}</strong>: {visibility.longDescription}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
### Examples
|
||||
|
||||
#### Request
|
||||
|
||||
```zsh
|
||||
curl -X QUERY https://kycnot.me/api/v1/service/get \
|
||||
@@ -93,12 +109,13 @@ curl -X QUERY https://kycnot.me/api/v1/service/get \
|
||||
-d '{"slug": "my-example-service"}'
|
||||
```
|
||||
|
||||
### Example Response
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Example Service",
|
||||
"description": "This is a description of my example service",
|
||||
"serviceVisibility": "PUBLIC",
|
||||
"verificationStatus": "VERIFICATION_SUCCESS",
|
||||
"verificationStatusInfo": {
|
||||
"value": "VERIFICATION_SUCCESS",
|
||||
@@ -107,6 +124,7 @@ curl -X QUERY https://kycnot.me/api/v1/service/get \
|
||||
"labelShort": "Verified",
|
||||
"description": "Thoroughly tested and verified by the team. But things might change, this is not a guarantee."
|
||||
},
|
||||
"verifiedAt": "2025-01-20T07:12:29.393Z",
|
||||
"kycLevel": 0,
|
||||
"kycLevelInfo": {
|
||||
"value": 0,
|
||||
@@ -119,6 +137,7 @@ curl -X QUERY https://kycnot.me/api/v1/service/get \
|
||||
"slug": "exchange"
|
||||
}
|
||||
],
|
||||
"listedAt": "2025-05-31T19:09:18.043Z",
|
||||
"serviceUrls": [
|
||||
"https://example.com",
|
||||
"http://c9ikae0fdidzh1ufrzp022e5uqfvz6ofxlkycz59cvo6fdxjgx7ekl9e.onion"
|
||||
@@ -128,7 +147,7 @@ curl -X QUERY https://kycnot.me/api/v1/service/get \
|
||||
}
|
||||
```
|
||||
|
||||
### Error Responses
|
||||
#### Error Responses
|
||||
|
||||
**404 Not Found**: Service not found
|
||||
|
||||
|
||||
@@ -218,16 +218,12 @@ const servicesQMatch = filters.q ? await findServicesBySimilarity(filters.q) : n
|
||||
|
||||
const where = {
|
||||
id: servicesQMatch ? { in: servicesQMatch.map(({ id }) => id) } : undefined,
|
||||
listedAt: {
|
||||
lte: new Date(),
|
||||
},
|
||||
listedAt: { lte: new Date() },
|
||||
categories: filters.categories.length ? { some: { slug: { in: filters.categories } } } : undefined,
|
||||
verificationStatus: {
|
||||
in: includeScams ? uniq([...filters.verification, 'VERIFICATION_FAILED'] as const) : filters.verification,
|
||||
},
|
||||
serviceVisibility: {
|
||||
in: ['PUBLIC', 'ARCHIVED'],
|
||||
},
|
||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
||||
overallScore: { gte: filters['min-score'] },
|
||||
acceptedCurrencies: filters.currencies.length
|
||||
? filters['currency-mode'] === 'and'
|
||||
@@ -319,9 +315,8 @@ const [categories, [services, totalServices], countCommunityOnly, attributes] =
|
||||
select: {
|
||||
services: {
|
||||
where: {
|
||||
serviceVisibility: {
|
||||
in: ['PUBLIC', 'ARCHIVED'],
|
||||
},
|
||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
||||
listedAt: { lte: new Date() },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -94,7 +94,16 @@ const user = await Astro.locals.banners.try('user', async () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
where: { service: { serviceVisibility: 'PUBLIC' } },
|
||||
where: {
|
||||
service: {
|
||||
listedAt: {
|
||||
lte: new Date(),
|
||||
},
|
||||
serviceVisibility: {
|
||||
in: ['PUBLIC', 'ARCHIVED'],
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 5,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user