fix: add admin

This commit is contained in:
2026-02-14 10:14:49 +01:00
parent 2e02d6d763
commit f2b690f3f5
86 changed files with 1057 additions and 1939 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
.env

3
.env
View File

@@ -1 +1,4 @@
NUXT_VATSTACK_API_KEY=pk_live_3038ce089ee7197879fcf95e624da019 NUXT_VATSTACK_API_KEY=pk_live_3038ce089ee7197879fcf95e624da019
NUXT_DB=mongodb+srv://development:35XRgDMsPkb1opbp@cluster0.ajj3s.mongodb.net/vat-api
NUXT_REDIS_URL=redis://:eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81@localhost:6379
NUXT_ADMIN_PASSWORD=changeme

View File

@@ -1,2 +1,5 @@
# Vatstack API key for live VAT rate data (optional — falls back to hardcoded rates) # Vatstack API key for live VAT rate data (optional — falls back to hardcoded rates)
NUXT_VATSTACK_API_KEY= NUXT_VATSTACK_API_KEY=
NUXT_DB=mongodb+srv://development:35XRgDMsPkb1opbp@cluster0.ajj3s.mongodb.net/vat-api
NUXT_REDIS_URL=redis://:eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81@localhost:6379
NUXT_ADMIN_PASSWORD=changeme

1
.nuxt/imports.d.ts vendored
View File

@@ -32,3 +32,4 @@ export { definePageMeta } from '../node_modules/nuxt/dist/pages/runtime/composab
export { defineLazyHydrationComponent } from '#app/composables/lazy-hydration'; export { defineLazyHydrationComponent } from '#app/composables/lazy-hydration';
export { useScrollAnimation } from '../app/composables/useScrollAnimation'; export { useScrollAnimation } from '../app/composables/useScrollAnimation';
export { useVatRates, VatRate } from '../app/composables/useVatRates'; export { useVatRates, VatRate } from '../app/composables/useVatRates';
export { useNuxtDevTools } from '../node_modules/@nuxt/devtools/dist/runtime/use-nuxt-devtools';

View File

@@ -1,4 +1,4 @@
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 2/13/2026, 10:13:46 PM // generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 2/14/2026, 10:13:39 AM
import "@nuxtjs/tailwindcss/config-ctx" import "@nuxtjs/tailwindcss/config-ctx"
import configMerger from "@nuxtjs/tailwindcss/merger"; import configMerger from "@nuxtjs/tailwindcss/merger";

View File

@@ -104,7 +104,7 @@
"./imports" "./imports"
], ],
"#app-manifest": [ "#app-manifest": [
"./manifest/meta/28eed3f6-8bb7-4ce5-bd5e-c2f1ff73389b" "./manifest/meta/dev"
], ],
"#components": [ "#components": [
"./components" "./components"
@@ -181,7 +181,7 @@
"../node_modules/dist/runtime/server", "../node_modules/dist/runtime/server",
"../node_modules/*.*", "../node_modules/*.*",
"../node_modules/dist/*.*", "../node_modules/dist/*.*",
"../.output", "dev",
"../server" "../server"
] ]
} }

View File

@@ -104,7 +104,7 @@
"./imports" "./imports"
], ],
"#app-manifest": [ "#app-manifest": [
"./manifest/meta/28eed3f6-8bb7-4ce5-bd5e-c2f1ff73389b" "./manifest/meta/dev"
], ],
"#components": [ "#components": [
"./components" "./components"

View File

@@ -104,7 +104,7 @@
"./imports" "./imports"
], ],
"#app-manifest": [ "#app-manifest": [
"./manifest/meta/28eed3f6-8bb7-4ce5-bd5e-c2f1ff73389b" "./manifest/meta/dev"
], ],
"#build": [ "#build": [
"." "."

View File

@@ -12,6 +12,8 @@ declare module "#build/paths.mjs";
declare module "#build/root-component.mjs"; declare module "#build/root-component.mjs";
declare module "#build/plugins.server.mjs"; declare module "#build/plugins.server.mjs";
declare module "#build/test-component-wrapper.mjs"; declare module "#build/test-component-wrapper.mjs";
declare module "#build/devtools/settings.mjs";
declare module "#build/runtime.vue-devtools-client.nCg_nTMck4Vjq9J_TXBbUMCbducTRhoEa7hkWr4xMsk.js";
declare module "#build/routes.mjs"; declare module "#build/routes.mjs";
declare module "#build/pages.mjs"; declare module "#build/pages.mjs";
declare module "#build/router.options.mjs"; declare module "#build/router.options.mjs";

View File

@@ -113,6 +113,7 @@ declare global {
const useModel: typeof import('vue').useModel const useModel: typeof import('vue').useModel
const useNuxtApp: typeof import('../../node_modules/nuxt/dist/app/nuxt').useNuxtApp const useNuxtApp: typeof import('../../node_modules/nuxt/dist/app/nuxt').useNuxtApp
const useNuxtData: typeof import('../../node_modules/nuxt/dist/app/composables/asyncData').useNuxtData const useNuxtData: typeof import('../../node_modules/nuxt/dist/app/composables/asyncData').useNuxtData
const useNuxtDevTools: typeof import('../../node_modules/@nuxt/devtools/dist/runtime/use-nuxt-devtools').useNuxtDevTools
const usePreviewMode: typeof import('../../node_modules/nuxt/dist/app/composables/preview').usePreviewMode const usePreviewMode: typeof import('../../node_modules/nuxt/dist/app/composables/preview').usePreviewMode
const useRequestEvent: typeof import('../../node_modules/nuxt/dist/app/composables/ssr').useRequestEvent const useRequestEvent: typeof import('../../node_modules/nuxt/dist/app/composables/ssr').useRequestEvent
const useRequestFetch: typeof import('../../node_modules/nuxt/dist/app/composables/ssr').useRequestFetch const useRequestFetch: typeof import('../../node_modules/nuxt/dist/app/composables/ssr').useRequestFetch
@@ -302,6 +303,7 @@ declare module 'vue' {
readonly useModel: UnwrapRef<typeof import('vue')['useModel']> readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useNuxtApp: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/nuxt')['useNuxtApp']> readonly useNuxtApp: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/nuxt')['useNuxtApp']>
readonly useNuxtData: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/asyncData')['useNuxtData']> readonly useNuxtData: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/asyncData')['useNuxtData']>
readonly useNuxtDevTools: UnwrapRef<typeof import('../../node_modules/@nuxt/devtools/dist/runtime/use-nuxt-devtools')['useNuxtDevTools']>
readonly usePreviewMode: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/preview')['usePreviewMode']> readonly usePreviewMode: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/preview')['usePreviewMode']>
readonly useRequestEvent: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/ssr')['useRequestEvent']> readonly useRequestEvent: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/ssr')['useRequestEvent']>
readonly useRequestFetch: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/ssr')['useRequestFetch']> readonly useRequestFetch: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/ssr')['useRequestFetch']>

View File

@@ -15,11 +15,14 @@ declare global {
const callNodeListener: typeof import('../../node_modules/h3').callNodeListener const callNodeListener: typeof import('../../node_modules/h3').callNodeListener
const clearResponseHeaders: typeof import('../../node_modules/h3').clearResponseHeaders const clearResponseHeaders: typeof import('../../node_modules/h3').clearResponseHeaders
const clearSession: typeof import('../../node_modules/h3').clearSession const clearSession: typeof import('../../node_modules/h3').clearSession
const closeDb: typeof import('../../server/utils/mongodb').closeDb
const closeRedis: typeof import('../../server/utils/redis').closeRedis
const createApp: typeof import('../../node_modules/h3').createApp const createApp: typeof import('../../node_modules/h3').createApp
const createAppEventHandler: typeof import('../../node_modules/h3').createAppEventHandler const createAppEventHandler: typeof import('../../node_modules/h3').createAppEventHandler
const createError: typeof import('../../node_modules/h3').createError const createError: typeof import('../../node_modules/h3').createError
const createEvent: typeof import('../../node_modules/h3').createEvent const createEvent: typeof import('../../node_modules/h3').createEvent
const createEventStream: typeof import('../../node_modules/h3').createEventStream const createEventStream: typeof import('../../node_modules/h3').createEventStream
const createRedisClient: typeof import('../../server/utils/redis').createRedisClient
const createRouter: typeof import('../../node_modules/h3').createRouter const createRouter: typeof import('../../node_modules/h3').createRouter
const defaultContentType: typeof import('../../node_modules/h3').defaultContentType const defaultContentType: typeof import('../../node_modules/h3').defaultContentType
const defineAppConfig: typeof import('../../node_modules/@nuxt/nitro-server/dist/runtime/utils/config').defineAppConfig const defineAppConfig: typeof import('../../node_modules/@nuxt/nitro-server/dist/runtime/utils/config').defineAppConfig
@@ -47,12 +50,14 @@ declare global {
const fromWebHandler: typeof import('../../node_modules/h3').fromWebHandler const fromWebHandler: typeof import('../../node_modules/h3').fromWebHandler
const getAllRates: typeof import('../../server/utils/vatRates').getAllRates const getAllRates: typeof import('../../server/utils/vatRates').getAllRates
const getCookie: typeof import('../../node_modules/h3').getCookie const getCookie: typeof import('../../node_modules/h3').getCookie
const getDb: typeof import('../../server/utils/mongodb').getDb
const getHeader: typeof import('../../node_modules/h3').getHeader const getHeader: typeof import('../../node_modules/h3').getHeader
const getHeaders: typeof import('../../node_modules/h3').getHeaders const getHeaders: typeof import('../../node_modules/h3').getHeaders
const getMethod: typeof import('../../node_modules/h3').getMethod const getMethod: typeof import('../../node_modules/h3').getMethod
const getProxyRequestHeaders: typeof import('../../node_modules/h3').getProxyRequestHeaders const getProxyRequestHeaders: typeof import('../../node_modules/h3').getProxyRequestHeaders
const getQuery: typeof import('../../node_modules/h3').getQuery const getQuery: typeof import('../../node_modules/h3').getQuery
const getRateByCode: typeof import('../../server/utils/vatRates').getRateByCode const getRateByCode: typeof import('../../server/utils/vatRates').getRateByCode
const getRedis: typeof import('../../server/utils/redis').getRedis
const getRequestFingerprint: typeof import('../../node_modules/h3').getRequestFingerprint const getRequestFingerprint: typeof import('../../node_modules/h3').getRequestFingerprint
const getRequestHeader: typeof import('../../node_modules/h3').getRequestHeader const getRequestHeader: typeof import('../../node_modules/h3').getRequestHeader
const getRequestHeaders: typeof import('../../node_modules/h3').getRequestHeaders const getRequestHeaders: typeof import('../../node_modules/h3').getRequestHeaders
@@ -83,6 +88,7 @@ declare global {
const isStream: typeof import('../../node_modules/h3').isStream const isStream: typeof import('../../node_modules/h3').isStream
const isWebResponse: typeof import('../../node_modules/h3').isWebResponse const isWebResponse: typeof import('../../node_modules/h3').isWebResponse
const lazyEventHandler: typeof import('../../node_modules/h3').lazyEventHandler const lazyEventHandler: typeof import('../../node_modules/h3').lazyEventHandler
const logRequest: typeof import('../../server/utils/requestLogger').logRequest
const nitroPlugin: typeof import('../../node_modules/nitropack/dist/runtime/internal/plugin').nitroPlugin const nitroPlugin: typeof import('../../node_modules/nitropack/dist/runtime/internal/plugin').nitroPlugin
const parseCookies: typeof import('../../node_modules/h3').parseCookies const parseCookies: typeof import('../../node_modules/h3').parseCookies
const promisifyNodeListener: typeof import('../../node_modules/h3').promisifyNodeListener const promisifyNodeListener: typeof import('../../node_modules/h3').promisifyNodeListener
@@ -135,6 +141,9 @@ declare global {
export type { EventHandler, EventHandlerRequest, EventHandlerResponse, EventHandlerObject, H3EventContext } from '../../node_modules/h3' export type { EventHandler, EventHandlerRequest, EventHandlerResponse, EventHandlerObject, H3EventContext } from '../../node_modules/h3'
import('../../node_modules/h3') import('../../node_modules/h3')
// @ts-ignore // @ts-ignore
export type { RequestLogEntry } from '../../server/utils/requestLogger'
import('../../server/utils/requestLogger')
// @ts-ignore
export type { VatRateResponse } from '../../server/utils/vatRates' export type { VatRateResponse } from '../../server/utils/vatRates'
import('../../server/utils/vatRates') import('../../server/utils/vatRates')
} }
@@ -152,4 +161,7 @@ export { defineTask, runTask } from 'nitropack/runtime/internal/task';
export { defineNitroErrorHandler } from 'nitropack/runtime/internal/error/utils'; export { defineNitroErrorHandler } from 'nitropack/runtime/internal/error/utils';
export { buildAssetsURL as __buildAssetsURL, publicAssetsURL as __publicAssetsURL } from '/home/bennet/source/vat-api/node_modules/@nuxt/nitro-server/dist/runtime/utils/paths'; export { buildAssetsURL as __buildAssetsURL, publicAssetsURL as __publicAssetsURL } from '/home/bennet/source/vat-api/node_modules/@nuxt/nitro-server/dist/runtime/utils/paths';
export { defineAppConfig } from '/home/bennet/source/vat-api/node_modules/@nuxt/nitro-server/dist/runtime/utils/config'; export { defineAppConfig } from '/home/bennet/source/vat-api/node_modules/@nuxt/nitro-server/dist/runtime/utils/config';
export { getDb, closeDb } from '/home/bennet/source/vat-api/server/utils/mongodb';
export { getRedis, createRedisClient, closeRedis } from '/home/bennet/source/vat-api/server/utils/redis';
export { logRequest } from '/home/bennet/source/vat-api/server/utils/requestLogger';
export { getAllRates, getRateByCode } from '/home/bennet/source/vat-api/server/utils/vatRates'; export { getAllRates, getRateByCode } from '/home/bennet/source/vat-api/server/utils/vatRates';

View File

@@ -3,6 +3,15 @@ import type { Serialize, Simplify } from "nitropack/types";
declare module "nitropack/types" { declare module "nitropack/types" {
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T
interface InternalApi { interface InternalApi {
'/api/admin/login': {
'post': Simplify<Serialize<Awaited<ReturnType<typeof import('../../server/api/admin/login.post').default>>>>
}
'/api/admin/requests': {
'get': Simplify<Serialize<Awaited<ReturnType<typeof import('../../server/api/admin/requests.get').default>>>>
}
'/api/admin/stats': {
'get': Simplify<Serialize<Awaited<ReturnType<typeof import('../../server/api/admin/stats.get').default>>>>
}
'/api/v1/rates/:code': { '/api/v1/rates/:code': {
'get': Simplify<Serialize<Awaited<ReturnType<typeof import('../../server/api/v1/rates/[code].get').default>>>> 'get': Simplify<Serialize<Awaited<ReturnType<typeof import('../../server/api/v1/rates/[code].get').default>>>>
} }

View File

@@ -9,18 +9,25 @@ type NuxtAppInjections =
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/revive-payload.client.js")> & InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/revive-payload.client.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/head/runtime/plugins/unhead.js")> & InjectionType<typeof import("../../node_modules/nuxt/dist/head/runtime/plugins/unhead.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/pages/runtime/plugins/router.js")> & InjectionType<typeof import("../../node_modules/nuxt/dist/pages/runtime/plugins/router.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/browser-devtools-timing.client.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/payload.client.js")> & InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/payload.client.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/dev-server-logs.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/navigation-repaint.client.js")> & InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/navigation-repaint.client.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/check-outdated-build.client.js")> & InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/check-outdated-build.client.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/revive-payload.server.js")> & InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/revive-payload.server.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/chunk-reload.client.js")> & InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/chunk-reload.client.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/pages/runtime/plugins/prefetch.client.js")> InjectionType<typeof import("../../node_modules/nuxt/dist/pages/runtime/plugins/prefetch.client.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/pages/runtime/plugins/check-if-page-unused.js")> &
InjectionType<typeof import("../../node_modules/@nuxt/devtools/dist/runtime/plugins/devtools.server.js")> &
InjectionType<typeof import("../../node_modules/@nuxt/devtools/dist/runtime/plugins/devtools.client.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/warn.dev.server.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/check-if-layout-used.js")>
declare module '#app' { declare module '#app' {
interface NuxtApp extends NuxtAppInjections { } interface NuxtApp extends NuxtAppInjections { }
interface NuxtAppLiterals { interface NuxtAppLiterals {
pluginName: 'nuxt:revive-payload:client' | 'nuxt:head' | 'nuxt:router' | 'nuxt:payload' | 'nuxt:revive-payload:server' | 'nuxt:chunk-reload' | 'nuxt:global-components' | 'nuxt:prefetch' pluginName: 'vue-devtools-client' | 'nuxt:revive-payload:client' | 'nuxt:head' | 'nuxt:router' | 'nuxt:browser-devtools-timing' | 'nuxt:payload' | 'nuxt:revive-payload:server' | 'nuxt:chunk-reload' | 'nuxt:global-components' | 'nuxt:prefetch' | 'nuxt:checkIfPageUnused' | 'nuxt:checkIfLayoutUsed'
} }
} }

View File

@@ -12,6 +12,12 @@ import { RuntimeConfig as UserRuntimeConfig, PublicRuntimeConfig as UserPublicRu
vatstackApiKey: string, vatstackApiKey: string,
db: string,
redisUrl: string,
adminPassword: string,
nitro: { nitro: {
envPrefix: string, envPrefix: string,
}, },

View File

@@ -0,0 +1,302 @@
<script setup lang="ts">
definePageMeta({ layout: false })
useHead({ title: 'Dashboard — vat-api.eu Admin' })
interface Stats {
totalRequests: { last24h: number; last7d: number; last30d: number }
uniqueIPs: number
topIPs: { ip: string; count: number; lastSeen: string }[]
requestsByHour: { hour: string; count: number }[]
}
interface RequestsResponse {
requests: { ip: string; path: string; method: string; statusCode: number; userAgent: string; timestamp: string }[]
total: number
page: number
limit: number
totalPages: number
}
const stats = ref<Stats | null>(null)
const reqs = ref<RequestsResponse | null>(null)
const error = ref('')
const page = ref(1)
const ipFilter = ref('')
let refreshTimer: ReturnType<typeof setInterval> | null = null
async function fetchStats() {
try {
stats.value = await $fetch<Stats>('/api/admin/stats')
} catch (e: any) {
if (e?.response?.status === 401) return navigateTo('/admin')
error.value = 'Failed to load stats'
}
}
async function fetchRequests() {
try {
const params: Record<string, string | number> = { page: page.value, limit: 50 }
if (ipFilter.value) params.ip = ipFilter.value
reqs.value = await $fetch<RequestsResponse>('/api/admin/requests', { params })
} catch (e: any) {
if (e?.response?.status === 401) return navigateTo('/admin')
}
}
function filterByIp(ip: string) {
ipFilter.value = ip
page.value = 1
fetchRequests()
}
function clearFilter() {
ipFilter.value = ''
page.value = 1
fetchRequests()
}
function changePage(p: number) {
page.value = p
fetchRequests()
}
function formatTime(ts: string) {
return new Date(ts).toLocaleString()
}
function reqsPerMinute() {
if (!stats.value) return '0'
return (stats.value.totalRequests.last24h / 1440).toFixed(1)
}
async function refresh() {
await Promise.all([fetchStats(), fetchRequests()])
}
onMounted(() => {
refresh()
refreshTimer = setInterval(refresh, 30_000)
})
onUnmounted(() => {
if (refreshTimer) clearInterval(refreshTimer)
})
</script>
<template>
<div class="min-h-screen bg-surface-soft">
<!-- Header -->
<header class="bg-white border-b border-surface-border">
<div class="section-container flex items-center justify-between h-16">
<div class="flex items-center gap-3">
<NuxtLink to="/" class="flex items-center gap-2 group">
<span class="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-eu-blue text-white font-heading font-bold text-sm transition-transform group-hover:scale-105">
V
</span>
<span class="font-heading font-bold text-lg text-ink">
vat-api<span class="text-eu-blue">.eu</span>
</span>
</NuxtLink>
<span class="text-ink-faint mx-1">/</span>
<span class="text-sm font-medium text-ink-secondary">Admin</span>
</div>
<NuxtLink
to="/admin"
class="text-sm text-ink-muted hover:text-ink transition-colors"
>
Sign out
</NuxtLink>
</div>
</header>
<main class="section-container py-8">
<div v-if="error" class="mb-6 px-4 py-3 rounded-xl bg-red-50 border border-red-200 text-red-700 text-sm">
{{ error }}
</div>
<!-- Summary cards -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div class="bg-white rounded-2xl border border-surface-border shadow-soft p-5">
<p class="text-xs font-medium text-ink-muted uppercase tracking-wide">Requests (24h)</p>
<p class="mt-1 text-2xl font-bold text-ink font-heading tabular-nums">
{{ stats?.totalRequests.last24h?.toLocaleString() ?? '...' }}
</p>
</div>
<div class="bg-white rounded-2xl border border-surface-border shadow-soft p-5">
<p class="text-xs font-medium text-ink-muted uppercase tracking-wide">Requests (7d)</p>
<p class="mt-1 text-2xl font-bold text-ink font-heading tabular-nums">
{{ stats?.totalRequests.last7d?.toLocaleString() ?? '...' }}
</p>
</div>
<div class="bg-white rounded-2xl border border-surface-border shadow-soft p-5">
<p class="text-xs font-medium text-ink-muted uppercase tracking-wide">Unique IPs (24h)</p>
<p class="mt-1 text-2xl font-bold text-ink font-heading tabular-nums">
{{ stats?.uniqueIPs?.toLocaleString() ?? '...' }}
</p>
</div>
<div class="bg-white rounded-2xl border border-surface-border shadow-soft p-5">
<p class="text-xs font-medium text-ink-muted uppercase tracking-wide">Reqs / min (avg)</p>
<p class="mt-1 text-2xl font-bold text-ink font-heading tabular-nums">
{{ stats ? reqsPerMinute() : '...' }}
</p>
</div>
</div>
<div class="grid lg:grid-cols-2 gap-6 mb-8">
<!-- Top IPs -->
<div class="bg-white rounded-2xl border border-surface-border shadow-soft overflow-hidden">
<div class="px-5 py-4 border-b border-surface-border bg-surface-soft">
<h2 class="text-sm font-semibold text-ink font-heading">Top IPs (24h)</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-surface-border">
<th class="text-left font-semibold text-ink-secondary px-5 py-2.5 font-heading">IP</th>
<th class="text-right font-semibold text-ink-secondary px-5 py-2.5 font-heading w-24">Count</th>
<th class="text-right font-semibold text-ink-secondary px-5 py-2.5 font-heading w-44">Last seen</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in stats?.topIPs"
:key="row.ip"
class="border-b border-surface-border/60 last:border-0 hover:bg-eu-blue-100/40 transition-colors cursor-pointer"
@click="filterByIp(row.ip)"
>
<td class="px-5 py-2.5">
<span class="font-mono text-xs text-ink">{{ row.ip }}</span>
</td>
<td class="px-5 py-2.5 text-right">
<span class="font-semibold text-ink tabular-nums">{{ row.count.toLocaleString() }}</span>
</td>
<td class="px-5 py-2.5 text-right text-ink-muted text-xs">
{{ formatTime(row.lastSeen) }}
</td>
</tr>
<tr v-if="!stats?.topIPs?.length">
<td colspan="3" class="px-5 py-8 text-center text-ink-muted">No data yet</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Requests by hour -->
<div class="bg-white rounded-2xl border border-surface-border shadow-soft overflow-hidden">
<div class="px-5 py-4 border-b border-surface-border bg-surface-soft">
<h2 class="text-sm font-semibold text-ink font-heading">Requests by Hour (24h)</h2>
</div>
<div class="p-5">
<div v-if="stats?.requestsByHour?.length" class="flex items-end gap-1 h-40">
<div
v-for="bar in stats.requestsByHour"
:key="bar.hour"
class="flex-1 flex flex-col items-center gap-1"
>
<span class="text-[10px] text-ink-muted tabular-nums">{{ bar.count }}</span>
<div
class="w-full bg-eu-blue/80 rounded-t transition-all"
:style="{ height: `${Math.max(4, (bar.count / Math.max(...stats.requestsByHour.map(b => b.count))) * 120)}px` }"
/>
<span class="text-[10px] text-ink-faint tabular-nums">{{ bar.hour }}</span>
</div>
</div>
<p v-else class="text-center text-ink-muted py-8">No data yet</p>
</div>
</div>
</div>
<!-- Recent requests -->
<div class="bg-white rounded-2xl border border-surface-border shadow-soft overflow-hidden">
<div class="px-5 py-4 border-b border-surface-border bg-surface-soft flex items-center justify-between">
<h2 class="text-sm font-semibold text-ink font-heading">
Recent Requests
<span v-if="ipFilter" class="ml-2 text-xs font-normal text-ink-muted">
filtered: {{ ipFilter }}
<button class="ml-1 text-eu-blue hover:underline" @click="clearFilter">clear</button>
</span>
</h2>
<span class="text-xs text-ink-muted tabular-nums">
{{ reqs?.total?.toLocaleString() ?? '...' }} total
</span>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-surface-border">
<th class="text-left font-semibold text-ink-secondary px-5 py-2.5 font-heading">IP</th>
<th class="text-left font-semibold text-ink-secondary px-5 py-2.5 font-heading">Method</th>
<th class="text-left font-semibold text-ink-secondary px-5 py-2.5 font-heading">Path</th>
<th class="text-center font-semibold text-ink-secondary px-5 py-2.5 font-heading w-20">Status</th>
<th class="text-right font-semibold text-ink-secondary px-5 py-2.5 font-heading w-44">Time</th>
</tr>
</thead>
<tbody>
<tr
v-for="(req, i) in reqs?.requests"
:key="i"
class="border-b border-surface-border/60 last:border-0 hover:bg-eu-blue-100/40 transition-colors"
>
<td class="px-5 py-2.5">
<button
class="font-mono text-xs text-eu-blue hover:underline"
@click="filterByIp(req.ip)"
>
{{ req.ip }}
</button>
</td>
<td class="px-5 py-2.5">
<span class="inline-flex px-2 py-0.5 rounded bg-surface-muted font-mono text-xs font-medium text-ink-secondary">
{{ req.method }}
</span>
</td>
<td class="px-5 py-2.5 font-mono text-xs text-ink">{{ req.path }}</td>
<td class="px-5 py-2.5 text-center">
<span
class="inline-flex px-2 py-0.5 rounded text-xs font-semibold tabular-nums"
:class="req.statusCode < 400 ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'"
>
{{ req.statusCode }}
</span>
</td>
<td class="px-5 py-2.5 text-right text-ink-muted text-xs">
{{ formatTime(req.timestamp) }}
</td>
</tr>
<tr v-if="!reqs?.requests?.length">
<td colspan="5" class="px-5 py-8 text-center text-ink-muted">No requests yet</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div v-if="reqs && reqs.totalPages > 1" class="px-5 py-3 border-t border-surface-border flex items-center justify-between">
<button
:disabled="page <= 1"
class="text-sm text-eu-blue hover:underline disabled:text-ink-faint disabled:no-underline"
@click="changePage(page - 1)"
>
Previous
</button>
<span class="text-xs text-ink-muted tabular-nums">
Page {{ reqs.page }} of {{ reqs.totalPages }}
</span>
<button
:disabled="page >= reqs.totalPages"
class="text-sm text-eu-blue hover:underline disabled:text-ink-faint disabled:no-underline"
@click="changePage(page + 1)"
>
Next
</button>
</div>
</div>
<p class="text-center text-xs text-ink-faint mt-6">
Auto-refreshes every 30 seconds.
</p>
</main>
</div>
</template>

74
app/pages/admin/index.vue Normal file
View File

@@ -0,0 +1,74 @@
<script setup lang="ts">
definePageMeta({ layout: false })
useHead({ title: 'Admin Login — vat-api.eu' })
const password = ref('')
const error = ref('')
const loading = ref(false)
async function login() {
error.value = ''
loading.value = true
try {
await $fetch('/api/admin/login', {
method: 'POST',
body: { password: password.value },
})
navigateTo('/admin/dashboard')
} catch {
error.value = 'Invalid password'
} finally {
loading.value = false
}
}
</script>
<template>
<div class="min-h-screen bg-surface-soft flex items-center justify-center px-4">
<div class="w-full max-w-sm">
<!-- Logo -->
<div class="text-center mb-8">
<NuxtLink to="/" class="inline-flex items-center gap-2 group">
<span class="inline-flex items-center justify-center w-10 h-10 rounded-xl bg-eu-blue text-white font-heading font-bold text-lg transition-transform group-hover:scale-105">
V
</span>
<span class="font-heading font-bold text-xl text-ink">
vat-api<span class="text-eu-blue">.eu</span>
</span>
</NuxtLink>
<p class="mt-3 text-sm text-ink-muted">Admin Dashboard</p>
</div>
<!-- Login card -->
<form
class="bg-white rounded-2xl border border-surface-border shadow-card p-6"
@submit.prevent="login"
>
<h1 class="text-title text-ink font-heading mb-6">Sign in</h1>
<div v-if="error" class="mb-4 px-4 py-2.5 rounded-xl bg-red-50 border border-red-200 text-red-700 text-sm">
{{ error }}
</div>
<label class="block text-sm font-medium text-ink-secondary mb-1.5">Password</label>
<input
v-model="password"
type="password"
required
autofocus
placeholder="Enter admin password"
class="w-full px-4 py-2.5 rounded-xl border border-surface-border bg-white text-sm text-ink placeholder:text-ink-faint focus:outline-none focus:ring-2 focus:ring-eu-blue/20 focus:border-eu-blue/40 transition-all"
/>
<button
type="submit"
:disabled="loading"
class="mt-5 w-full px-4 py-2.5 rounded-xl bg-eu-blue text-white font-semibold text-sm hover:bg-eu-blue-dark transition-all disabled:opacity-50"
>
{{ loading ? 'Signing in...' : 'Sign in' }}
</button>
</form>
</div>
</div>
</template>

View File

@@ -20,7 +20,10 @@ var _default = exports.default = defineNuxtConfig({
}, },
runtimeConfig: { runtimeConfig: {
vatstackApiKey: '' vatstackApiKey: '',
db: '',
redisUrl: '',
adminPassword: ''
}, },
app: { app: {
@@ -31,4 +34,4 @@ var _default = exports.default = defineNuxtConfig({
} }
} }
}); /* v9-40b9d46909467c59 */ }); /* v9-d85c2260d73c206e */

View File

@@ -1 +0,0 @@
import{S as a,a as s,U as r,V as u,W as o}from"./Dgz6sxCl.js";function d(t){const e=t||s();return e.ssrContext?.head||e.runWithContext(()=>{if(r()){const n=u(o);if(!n)throw new Error("[nuxt] [unhead] Missing Unhead instance.");return n}})}function i(t,e={}){const n=e.head||d(e.nuxt);return a(t,{head:n,...e})}export{i as u};

View File

@@ -1 +0,0 @@
import{_ as o,y as s,z as a,A as t,B as r}from"./Dgz6sxCl.js";import{u as i}from"./BGLmsXN0.js";const u={class:"antialiased bg-white dark:bg-[#020420] dark:text-white font-sans grid min-h-screen overflow-hidden place-content-center text-[#020420] tracking-wide"},l={class:"max-w-520px text-center"},c=["textContent"],d=["textContent"],p=["textContent"],f={__name:"error-500",props:{appName:{type:String,default:"Nuxt"},status:{type:Number,default:500},statusText:{type:String,default:"Internal server error"},description:{type:String,default:"This page is temporarily unavailable."},refresh:{type:String,default:"Refresh this page"}},setup(e){const n=e;return i({title:`${n.status} - ${n.statusText} | ${n.appName}`,script:[{innerHTML:`!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver(e=>{for(const o of e)if("childList"===o.type)for(const e of o.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&r(e)}).observe(document,{childList:!0,subtree:!0})}function r(e){if(e.ep)return;e.ep=!0;const r=function(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),"use-credentials"===e.crossOrigin?r.credentials="include":"anonymous"===e.crossOrigin?r.credentials="omit":r.credentials="same-origin",r}(e);fetch(e.href,r)}}();`}],style:[{innerHTML:'*,:after,:before{border-color:var(--un-default-border-color,#e5e7eb);border-style:solid;border-width:0;box-sizing:border-box}:after,:before{--un-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}h1,h2{font-size:inherit;font-weight:inherit}h1,h2,p{margin:0}*,:after,:before{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 transparent;--un-ring-shadow:0 0 transparent;--un-shadow-inset: ;--un-shadow:0 0 transparent;--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }'}]}),(h,m)=>(s(),a("div",u,[t("div",l,[t("h1",{class:"font-semibold leading-none mb-4 sm:text-[110px] tabular-nums text-[80px]",textContent:r(e.status)},null,8,c),t("h2",{class:"font-semibold mb-2 sm:text-3xl text-2xl",textContent:r(e.statusText)},null,8,d),t("p",{class:"mb-4 px-2 text-[#64748B] text-md",textContent:r(e.description)},null,8,p)])]))}},x=o(f,[["__scopeId","data-v-d349100d"]]);export{x as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.grid[data-v-204d37bf]{display:grid}.mb-2[data-v-204d37bf]{margin-bottom:.5rem}.mb-4[data-v-204d37bf]{margin-bottom:1rem}.max-w-520px[data-v-204d37bf]{max-width:520px}.min-h-screen[data-v-204d37bf]{min-height:100vh}.w-full[data-v-204d37bf]{width:100%}.flex[data-v-204d37bf]{display:flex}.place-content-center[data-v-204d37bf]{place-content:center}.items-center[data-v-204d37bf]{align-items:center}.justify-center[data-v-204d37bf]{justify-content:center}.overflow-hidden[data-v-204d37bf]{overflow:hidden}.bg-white[data-v-204d37bf]{--un-bg-opacity:1;background-color:rgb(255 255 255/var(--un-bg-opacity))}.px-2[data-v-204d37bf]{padding-left:.5rem;padding-right:.5rem}.text-center[data-v-204d37bf]{text-align:center}.text-\[80px\][data-v-204d37bf]{font-size:80px}.text-2xl[data-v-204d37bf]{font-size:1.5rem;line-height:2rem}.text-sm[data-v-204d37bf]{font-size:.875rem;line-height:1.25rem}.text-\[\#020420\][data-v-204d37bf]{--un-text-opacity:1;color:rgb(2 4 32/var(--un-text-opacity))}.text-\[\#64748B\][data-v-204d37bf]{--un-text-opacity:1;color:rgb(100 116 139/var(--un-text-opacity))}.hover\:text-\[\#00DC82\][data-v-204d37bf]:hover{--un-text-opacity:1;color:rgb(0 220 130/var(--un-text-opacity))}.font-medium[data-v-204d37bf]{font-weight:500}.font-semibold[data-v-204d37bf]{font-weight:600}.leading-none[data-v-204d37bf]{line-height:1}.tracking-wide[data-v-204d37bf]{letter-spacing:.025em}.font-sans[data-v-204d37bf]{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.tabular-nums[data-v-204d37bf]{--un-numeric-spacing:tabular-nums;font-variant-numeric:var(--un-ordinal) var(--un-slashed-zero) var(--un-numeric-figure) var(--un-numeric-spacing) var(--un-numeric-fraction)}.underline[data-v-204d37bf]{text-decoration-line:underline}.underline-offset-3[data-v-204d37bf]{text-underline-offset:3px}.antialiased[data-v-204d37bf]{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media(prefers-color-scheme:dark){.dark\:bg-\[\#020420\][data-v-204d37bf]{--un-bg-opacity:1;background-color:rgb(2 4 32/var(--un-bg-opacity))}.dark\:text-white[data-v-204d37bf]{--un-text-opacity:1;color:rgb(255 255 255/var(--un-text-opacity))}}@media(min-width:640px){.sm\:text-\[110px\][data-v-204d37bf]{font-size:110px}.sm\:text-3xl[data-v-204d37bf]{font-size:1.875rem;line-height:2.25rem}}

View File

@@ -1 +0,0 @@
.grid[data-v-d349100d]{display:grid}.mb-2[data-v-d349100d]{margin-bottom:.5rem}.mb-4[data-v-d349100d]{margin-bottom:1rem}.max-w-520px[data-v-d349100d]{max-width:520px}.min-h-screen[data-v-d349100d]{min-height:100vh}.place-content-center[data-v-d349100d]{place-content:center}.overflow-hidden[data-v-d349100d]{overflow:hidden}.bg-white[data-v-d349100d]{--un-bg-opacity:1;background-color:rgb(255 255 255/var(--un-bg-opacity))}.px-2[data-v-d349100d]{padding-left:.5rem;padding-right:.5rem}.text-center[data-v-d349100d]{text-align:center}.text-\[80px\][data-v-d349100d]{font-size:80px}.text-2xl[data-v-d349100d]{font-size:1.5rem;line-height:2rem}.text-\[\#020420\][data-v-d349100d]{--un-text-opacity:1;color:rgb(2 4 32/var(--un-text-opacity))}.text-\[\#64748B\][data-v-d349100d]{--un-text-opacity:1;color:rgb(100 116 139/var(--un-text-opacity))}.font-semibold[data-v-d349100d]{font-weight:600}.leading-none[data-v-d349100d]{line-height:1}.tracking-wide[data-v-d349100d]{letter-spacing:.025em}.font-sans[data-v-d349100d]{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.tabular-nums[data-v-d349100d]{--un-numeric-spacing:tabular-nums;font-variant-numeric:var(--un-ordinal) var(--un-slashed-zero) var(--un-numeric-figure) var(--un-numeric-spacing) var(--un-numeric-fraction)}.antialiased[data-v-d349100d]{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media(prefers-color-scheme:dark){.dark\:bg-\[\#020420\][data-v-d349100d]{--un-bg-opacity:1;background-color:rgb(2 4 32/var(--un-bg-opacity))}.dark\:text-white[data-v-d349100d]{--un-text-opacity:1;color:rgb(255 255 255/var(--un-text-opacity))}}@media(min-width:640px){.sm\:text-\[110px\][data-v-d349100d]{font-size:110px}.sm\:text-3xl[data-v-d349100d]{font-size:1.875rem;line-height:2.25rem}}

View File

@@ -1,23 +0,0 @@
import { hasInjectionContext, inject } from "vue";
import { useHead as useHead$1, headSymbol } from "/home/bennet/source/vat-api/node_modules/@unhead/vue/dist/index.mjs";
import { a as useNuxtApp } from "../server.mjs";
function injectHead(nuxtApp) {
const nuxt = nuxtApp || useNuxtApp();
return nuxt.ssrContext?.head || nuxt.runWithContext(() => {
if (hasInjectionContext()) {
const head = inject(headSymbol);
if (!head) {
throw new Error("[nuxt] [unhead] Missing Unhead instance.");
}
return head;
}
});
}
function useHead(input, options = {}) {
const head = options.head || injectHead(options.nuxt);
return useHead$1(input, { head, ...options });
}
export {
useHead as u
};
//# sourceMappingURL=composables-Piy9capG.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"composables-Piy9capG.js","sources":["../../../../../../nuxt/dist/head/runtime/composables.js"],"sourcesContent":["import { hasInjectionContext, inject } from \"vue\";\nimport {\n useHead as headCore,\n useHeadSafe as headSafe,\n headSymbol,\n useSeoMeta as seoMeta,\n useServerHead as serverHead,\n useServerHeadSafe as serverHeadSafe,\n useServerSeoMeta as serverSeoMeta\n} from \"@unhead/vue\";\nimport { useNuxtApp } from \"#app/nuxt\";\nexport function injectHead(nuxtApp) {\n const nuxt = nuxtApp || useNuxtApp();\n return nuxt.ssrContext?.head || nuxt.runWithContext(() => {\n if (hasInjectionContext()) {\n const head = inject(headSymbol);\n if (!head) {\n throw new Error(\"[nuxt] [unhead] Missing Unhead instance.\");\n }\n return head;\n }\n });\n}\nexport function useHead(input, options = {}) {\n const head = options.head || injectHead(options.nuxt);\n return headCore(input, { head, ...options });\n}\nexport function useHeadSafe(input, options = {}) {\n const head = options.head || injectHead(options.nuxt);\n return headSafe(input, { head, ...options });\n}\nexport function useSeoMeta(input, options = {}) {\n const head = options.head || injectHead(options.nuxt);\n return seoMeta(input, { head, ...options });\n}\nexport function useServerHead(input, options = {}) {\n const head = options.head || injectHead(options.nuxt);\n return serverHead(input, { head, ...options });\n}\nexport function useServerHeadSafe(input, options = {}) {\n const head = options.head || injectHead(options.nuxt);\n return serverHeadSafe(input, { head, ...options });\n}\nexport function useServerSeoMeta(input, options = {}) {\n const head = options.head || injectHead(options.nuxt);\n return serverSeoMeta(input, { head, ...options });\n}\n"],"names":["headCore"],"mappings":";;;AAWO,SAAS,WAAW,SAAS;AAClC,QAAM,OAAO,WAAW,WAAU;AAClC,SAAO,KAAK,YAAY,QAAQ,KAAK,eAAe,MAAM;AACxD,QAAI,oBAAmB,GAAI;AACzB,YAAM,OAAO,OAAO,UAAU;AAC9B,UAAI,CAAC,MAAM;AACT,cAAM,IAAI,MAAM,0CAA0C;AAAA,MAC5D;AACA,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;AACO,SAAS,QAAQ,OAAO,UAAU,IAAI;AAC3C,QAAM,OAAO,QAAQ,QAAQ,WAAW,QAAQ,IAAI;AACpD,SAAOA,UAAS,OAAO,EAAE,MAAM,GAAG,QAAO,CAAE;AAC7C;"}

View File

@@ -1 +0,0 @@
{"file":"composables-Piy9capG.js","mappings":";;;AAWO,SAAS,WAAW,SAAS;AAClC,QAAM,OAAO,WAAW,WAAU;AAClC,SAAO,KAAK,YAAY,QAAQ,KAAK,eAAe,MAAM;AACxD,QAAI,oBAAmB,GAAI;AACzB,YAAM,OAAO,OAAO,UAAU;AAC9B,UAAI,CAAC,MAAM;AACT,cAAM,IAAI,MAAM,0CAA0C;AAAA,MAC5D;AACA,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;AACO,SAAS,QAAQ,OAAO,UAAU,IAAI;AAC3C,QAAM,OAAO,QAAQ,QAAQ,WAAW,QAAQ,IAAI;AACpD,SAAOA,UAAS,OAAO,EAAE,MAAM,GAAG,QAAO,CAAE;AAC7C;","names":["headCore"],"sources":["../../../../../../nuxt/dist/head/runtime/composables.js"],"sourcesContent":["import { hasInjectionContext, inject } from \"vue\";\nimport {\n useHead as headCore,\n useHeadSafe as headSafe,\n headSymbol,\n useSeoMeta as seoMeta,\n useServerHead as serverHead,\n useServerHeadSafe as serverHeadSafe,\n useServerSeoMeta as serverSeoMeta\n} from \"@unhead/vue\";\nimport { useNuxtApp } from \"#app/nuxt\";\nexport function injectHead(nuxtApp) {\n const nuxt = nuxtApp || useNuxtApp();\n return nuxt.ssrContext?.head || nuxt.runWithContext(() => {\n if (hasInjectionContext()) {\n const head = inject(headSymbol);\n if (!head) {\n throw new Error(\"[nuxt] [unhead] Missing Unhead instance.\");\n }\n return head;\n }\n });\n}\nexport function useHead(input, options = {}) {\n const head = options.head || injectHead(options.nuxt);\n return headCore(input, { head, ...options });\n}\nexport function useHeadSafe(input, options = {}) {\n const head = options.head || injectHead(options.nuxt);\n return headSafe(input, { head, ...options });\n}\nexport function useSeoMeta(input, options = {}) {\n const head = options.head || injectHead(options.nuxt);\n return seoMeta(input, { head, ...options });\n}\nexport function useServerHead(input, options = {}) {\n const head = options.head || injectHead(options.nuxt);\n return serverHead(input, { head, ...options });\n}\nexport function useServerHeadSafe(input, options = {}) {\n const head = options.head || injectHead(options.nuxt);\n return serverHeadSafe(input, { head, ...options });\n}\nexport function useServerSeoMeta(input, options = {}) {\n const head = options.head || injectHead(options.nuxt);\n return serverSeoMeta(input, { head, ...options });\n}\n"],"version":3}

View File

@@ -1,369 +0,0 @@
import { defineComponent, shallowRef, h, resolveComponent, computed, mergeProps, withCtx, createTextVNode, toDisplayString, useSSRContext } from "vue";
import { parseQuery, hasProtocol, joinURL, withTrailingSlash, withoutTrailingSlash } from "/home/bennet/source/vat-api/node_modules/ufo/dist/index.mjs";
import { u as useRouter, e as encodeRoutePath, r as resolveRouteObject, n as navigateTo, a as useNuxtApp, b as useRuntimeConfig, c as nuxtLinkDefaults, _ as _export_sfc } from "../server.mjs";
import { ssrRenderAttrs, ssrInterpolate, ssrRenderComponent } from "vue/server-renderer";
import { u as useHead } from "./composables-Piy9capG.js";
import "/home/bennet/source/vat-api/node_modules/ofetch/dist/node.mjs";
import "#internal/nuxt/paths";
import "/home/bennet/source/vat-api/node_modules/hookable/dist/index.mjs";
import "/home/bennet/source/vat-api/node_modules/unctx/dist/index.mjs";
import "/home/bennet/source/vat-api/node_modules/h3/dist/index.mjs";
import "vue-router";
import "/home/bennet/source/vat-api/node_modules/defu/dist/defu.mjs";
import "/home/bennet/source/vat-api/node_modules/@unhead/vue/dist/index.mjs";
const firstNonUndefined = (...args) => args.find((arg) => arg !== void 0);
// @__NO_SIDE_EFFECTS__
function defineNuxtLink(options) {
const componentName = options.componentName || "NuxtLink";
function isHashLinkWithoutHashMode(link) {
return typeof link === "string" && link.startsWith("#");
}
function resolveTrailingSlashBehavior(to, resolve, trailingSlash) {
const effectiveTrailingSlash = trailingSlash ?? options.trailingSlash;
if (!to || effectiveTrailingSlash !== "append" && effectiveTrailingSlash !== "remove") {
return to;
}
if (typeof to === "string") {
return applyTrailingSlashBehavior(to, effectiveTrailingSlash);
}
const path = "path" in to && to.path !== void 0 ? to.path : resolve(to).path;
const resolvedPath = {
...to,
name: void 0,
// named routes would otherwise always override trailing slash behavior
path: applyTrailingSlashBehavior(path, effectiveTrailingSlash)
};
return resolvedPath;
}
function useNuxtLink(props) {
const router = useRouter();
const config = useRuntimeConfig();
const hasTarget = computed(() => !!props.target && props.target !== "_self");
const isAbsoluteUrl = computed(() => {
const path = props.to || props.href || "";
return typeof path === "string" && hasProtocol(path, { acceptRelative: true });
});
const builtinRouterLink = resolveComponent("RouterLink");
const useBuiltinLink = builtinRouterLink && typeof builtinRouterLink !== "string" ? builtinRouterLink.useLink : void 0;
const isExternal = computed(() => {
if (props.external) {
return true;
}
const path = props.to || props.href || "";
if (typeof path === "object") {
return false;
}
return path === "" || isAbsoluteUrl.value;
});
const to = computed(() => {
const path = props.to || props.href || "";
if (isExternal.value) {
return path;
}
return resolveTrailingSlashBehavior(path, router.resolve, props.trailingSlash);
});
const link = isExternal.value ? void 0 : useBuiltinLink?.({ ...props, to });
const href = computed(() => {
const effectiveTrailingSlash = props.trailingSlash ?? options.trailingSlash;
if (!to.value || isAbsoluteUrl.value || isHashLinkWithoutHashMode(to.value)) {
return to.value;
}
if (isExternal.value) {
const path = typeof to.value === "object" && "path" in to.value ? resolveRouteObject(to.value) : to.value;
const href2 = typeof path === "object" ? router.resolve(path).href : path;
return applyTrailingSlashBehavior(href2, effectiveTrailingSlash);
}
if (typeof to.value === "object") {
return router.resolve(to.value)?.href ?? null;
}
return applyTrailingSlashBehavior(joinURL(config.app.baseURL, to.value), effectiveTrailingSlash);
});
return {
to,
hasTarget,
isAbsoluteUrl,
isExternal,
//
href,
isActive: link?.isActive ?? computed(() => to.value === router.currentRoute.value.path),
isExactActive: link?.isExactActive ?? computed(() => to.value === router.currentRoute.value.path),
route: link?.route ?? computed(() => router.resolve(to.value)),
async navigate(_e) {
await navigateTo(href.value, { replace: props.replace, external: isExternal.value || hasTarget.value });
}
};
}
return defineComponent({
name: componentName,
props: {
// Routing
to: {
type: [String, Object],
default: void 0,
required: false
},
href: {
type: [String, Object],
default: void 0,
required: false
},
// Attributes
target: {
type: String,
default: void 0,
required: false
},
rel: {
type: String,
default: void 0,
required: false
},
noRel: {
type: Boolean,
default: void 0,
required: false
},
// Prefetching
prefetch: {
type: Boolean,
default: void 0,
required: false
},
prefetchOn: {
type: [String, Object],
default: void 0,
required: false
},
noPrefetch: {
type: Boolean,
default: void 0,
required: false
},
// Styling
activeClass: {
type: String,
default: void 0,
required: false
},
exactActiveClass: {
type: String,
default: void 0,
required: false
},
prefetchedClass: {
type: String,
default: void 0,
required: false
},
// Vue Router's `<RouterLink>` additional props
replace: {
type: Boolean,
default: void 0,
required: false
},
ariaCurrentValue: {
type: String,
default: void 0,
required: false
},
// Edge cases handling
external: {
type: Boolean,
default: void 0,
required: false
},
// Slot API
custom: {
type: Boolean,
default: void 0,
required: false
},
// Behavior
trailingSlash: {
type: String,
default: void 0,
required: false
}
},
useLink: useNuxtLink,
setup(props, { slots }) {
const router = useRouter();
const { to, href, navigate, isExternal, hasTarget, isAbsoluteUrl } = useNuxtLink(props);
shallowRef(false);
const el = void 0;
const elRef = void 0;
async function prefetch(nuxtApp = useNuxtApp()) {
{
return;
}
}
return () => {
if (!isExternal.value && !hasTarget.value && !isHashLinkWithoutHashMode(to.value)) {
const routerLinkProps = {
ref: elRef,
to: to.value,
activeClass: props.activeClass || options.activeClass,
exactActiveClass: props.exactActiveClass || options.exactActiveClass,
replace: props.replace,
ariaCurrentValue: props.ariaCurrentValue,
custom: props.custom
};
if (!props.custom) {
routerLinkProps.rel = props.rel || void 0;
}
return h(
resolveComponent("RouterLink"),
routerLinkProps,
slots.default
);
}
const target = props.target || null;
const rel = firstNonUndefined(
// converts `""` to `null` to prevent the attribute from being added as empty (`rel=""`)
props.noRel ? "" : props.rel,
options.externalRelAttribute,
/*
* A fallback rel of `noopener noreferrer` is applied for external links or links that open in a new tab.
* This solves a reverse tabnapping security flaw in browsers pre-2021 as well as improving privacy.
*/
isAbsoluteUrl.value || hasTarget.value ? "noopener noreferrer" : ""
) || null;
if (props.custom) {
if (!slots.default) {
return null;
}
return slots.default({
href: href.value,
navigate,
prefetch,
get route() {
if (!href.value) {
return void 0;
}
const url = new URL(href.value, "http://localhost");
return {
path: url.pathname,
fullPath: url.pathname,
get query() {
return parseQuery(url.search);
},
hash: url.hash,
params: {},
name: void 0,
matched: [],
redirectedFrom: void 0,
meta: {},
href: href.value
};
},
rel,
target,
isExternal: isExternal.value || hasTarget.value,
isActive: false,
isExactActive: false
});
}
return h("a", {
ref: el,
href: href.value || null,
// converts `""` to `null` to prevent the attribute from being added as empty (`href=""`)
rel,
target,
onClick: async (event) => {
if (isExternal.value || hasTarget.value) {
return;
}
event.preventDefault();
try {
const encodedHref = encodeRoutePath(href.value);
return await (props.replace ? router.replace(encodedHref) : router.push(encodedHref));
} finally {
}
}
}, slots.default?.());
};
}
});
}
const __nuxt_component_0 = /* @__PURE__ */ defineNuxtLink(nuxtLinkDefaults);
function applyTrailingSlashBehavior(to, trailingSlash) {
const normalizeFn = trailingSlash === "append" ? withTrailingSlash : withoutTrailingSlash;
const hasProtocolDifferentFromHttp = hasProtocol(to) && !to.startsWith("http");
if (hasProtocolDifferentFromHttp) {
return to;
}
return normalizeFn(to, true);
}
const _sfc_main = {
__name: "error-404",
__ssrInlineRender: true,
props: {
appName: {
type: String,
default: "Nuxt"
},
status: {
type: Number,
default: 404
},
statusText: {
type: String,
default: "Page not found"
},
description: {
type: String,
default: "Sorry, the page you are looking for could not be found."
},
backHome: {
type: String,
default: "Go back home"
}
},
setup(__props) {
const props = __props;
useHead({
title: `${props.status} - ${props.statusText} | ${props.appName}`,
script: [
{
innerHTML: `!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver(e=>{for(const o of e)if("childList"===o.type)for(const e of o.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&r(e)}).observe(document,{childList:!0,subtree:!0})}function r(e){if(e.ep)return;e.ep=!0;const r=function(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),"use-credentials"===e.crossOrigin?r.credentials="include":"anonymous"===e.crossOrigin?r.credentials="omit":r.credentials="same-origin",r}(e);fetch(e.href,r)}}();`
}
],
style: [
{
innerHTML: `*,:after,:before{border-color:var(--un-default-border-color,#e5e7eb);border-style:solid;border-width:0;box-sizing:border-box}:after,:before{--un-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}h1,h2{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}h1,h2,p{margin:0}*,:after,:before{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 transparent;--un-ring-shadow:0 0 transparent;--un-shadow-inset: ;--un-shadow:0 0 transparent;--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }`
}
]
});
return (_ctx, _push, _parent, _attrs) => {
const _component_NuxtLink = __nuxt_component_0;
_push(`<div${ssrRenderAttrs(mergeProps({ class: "antialiased bg-white dark:bg-[#020420] dark:text-white font-sans grid min-h-screen overflow-hidden place-content-center text-[#020420] tracking-wide" }, _attrs))} data-v-204d37bf><div class="max-w-520px text-center" data-v-204d37bf><h1 class="font-semibold leading-none mb-4 sm:text-[110px] tabular-nums text-[80px]" data-v-204d37bf>${ssrInterpolate(__props.status)}</h1><h2 class="font-semibold mb-2 sm:text-3xl text-2xl" data-v-204d37bf>${ssrInterpolate(__props.statusText)}</h2><p class="mb-4 px-2 text-[#64748B] text-md" data-v-204d37bf>${ssrInterpolate(__props.description)}</p><div class="flex items-center justify-center w-full" data-v-204d37bf>`);
_push(ssrRenderComponent(_component_NuxtLink, {
to: "/",
class: "font-medium hover:text-[#00DC82] text-sm underline underline-offset-3"
}, {
default: withCtx((_, _push2, _parent2, _scopeId) => {
if (_push2) {
_push2(`${ssrInterpolate(__props.backHome)}`);
} else {
return [
createTextVNode(toDisplayString(__props.backHome), 1)
];
}
}),
_: 1
}, _parent));
_push(`</div></div></div>`);
};
}
};
const _sfc_setup = _sfc_main.setup;
_sfc_main.setup = (props, ctx) => {
const ssrContext = useSSRContext();
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/nuxt/dist/app/components/error-404.vue");
return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
};
const error404 = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-204d37bf"]]);
export {
error404 as default
};
//# sourceMappingURL=error-404-BY_x-_oz.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +0,0 @@
const error404_vue_vue_type_style_index_0_scoped_204d37bf_lang = ".grid[data-v-204d37bf]{display:grid}.mb-2[data-v-204d37bf]{margin-bottom:.5rem}.mb-4[data-v-204d37bf]{margin-bottom:1rem}.max-w-520px[data-v-204d37bf]{max-width:520px}.min-h-screen[data-v-204d37bf]{min-height:100vh}.w-full[data-v-204d37bf]{width:100%}.flex[data-v-204d37bf]{display:flex}.place-content-center[data-v-204d37bf]{place-content:center}.items-center[data-v-204d37bf]{align-items:center}.justify-center[data-v-204d37bf]{justify-content:center}.overflow-hidden[data-v-204d37bf]{overflow:hidden}.bg-white[data-v-204d37bf]{--un-bg-opacity:1;background-color:rgb(255 255 255/var(--un-bg-opacity))}.px-2[data-v-204d37bf]{padding-left:.5rem;padding-right:.5rem}.text-center[data-v-204d37bf]{text-align:center}.text-\\[80px\\][data-v-204d37bf]{font-size:80px}.text-2xl[data-v-204d37bf]{font-size:1.5rem;line-height:2rem}.text-sm[data-v-204d37bf]{font-size:.875rem;line-height:1.25rem}.text-\\[\\#020420\\][data-v-204d37bf]{--un-text-opacity:1;color:rgb(2 4 32/var(--un-text-opacity))}.text-\\[\\#64748B\\][data-v-204d37bf]{--un-text-opacity:1;color:rgb(100 116 139/var(--un-text-opacity))}.hover\\:text-\\[\\#00DC82\\][data-v-204d37bf]:hover{--un-text-opacity:1;color:rgb(0 220 130/var(--un-text-opacity))}.font-medium[data-v-204d37bf]{font-weight:500}.font-semibold[data-v-204d37bf]{font-weight:600}.leading-none[data-v-204d37bf]{line-height:1}.tracking-wide[data-v-204d37bf]{letter-spacing:.025em}.font-sans[data-v-204d37bf]{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.tabular-nums[data-v-204d37bf]{--un-numeric-spacing:tabular-nums;font-variant-numeric:var(--un-ordinal) var(--un-slashed-zero) var(--un-numeric-figure) var(--un-numeric-spacing) var(--un-numeric-fraction)}.underline[data-v-204d37bf]{text-decoration-line:underline}.underline-offset-3[data-v-204d37bf]{text-underline-offset:3px}.antialiased[data-v-204d37bf]{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media(prefers-color-scheme:dark){.dark\\:bg-\\[\\#020420\\][data-v-204d37bf]{--un-bg-opacity:1;background-color:rgb(2 4 32/var(--un-bg-opacity))}.dark\\:text-white[data-v-204d37bf]{--un-text-opacity:1;color:rgb(255 255 255/var(--un-text-opacity))}}@media(min-width:640px){.sm\\:text-\\[110px\\][data-v-204d37bf]{font-size:110px}.sm\\:text-3xl[data-v-204d37bf]{font-size:1.875rem;line-height:2.25rem}}";
export {
error404_vue_vue_type_style_index_0_scoped_204d37bf_lang as default
};
//# sourceMappingURL=error-404-styles-1.mjs-Bae73Gon.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"error-404-styles-1.mjs-Bae73Gon.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}

View File

@@ -1 +0,0 @@
{"file":"error-404-styles-1.mjs-Bae73Gon.js","mappings":";","names":[],"sources":[],"sourcesContent":[],"version":3}

View File

@@ -1,4 +0,0 @@
import style_0 from "./error-404-styles-1.mjs-Bae73Gon.js";
export default [
style_0
]

View File

@@ -1,69 +0,0 @@
import { mergeProps, useSSRContext } from "vue";
import { ssrRenderAttrs, ssrInterpolate } from "vue/server-renderer";
import { _ as _export_sfc } from "../server.mjs";
import { u as useHead } from "./composables-Piy9capG.js";
import "/home/bennet/source/vat-api/node_modules/ofetch/dist/node.mjs";
import "#internal/nuxt/paths";
import "/home/bennet/source/vat-api/node_modules/hookable/dist/index.mjs";
import "/home/bennet/source/vat-api/node_modules/unctx/dist/index.mjs";
import "/home/bennet/source/vat-api/node_modules/h3/dist/index.mjs";
import "vue-router";
import "/home/bennet/source/vat-api/node_modules/defu/dist/defu.mjs";
import "/home/bennet/source/vat-api/node_modules/ufo/dist/index.mjs";
import "/home/bennet/source/vat-api/node_modules/@unhead/vue/dist/index.mjs";
const _sfc_main = {
__name: "error-500",
__ssrInlineRender: true,
props: {
appName: {
type: String,
default: "Nuxt"
},
status: {
type: Number,
default: 500
},
statusText: {
type: String,
default: "Internal server error"
},
description: {
type: String,
default: "This page is temporarily unavailable."
},
refresh: {
type: String,
default: "Refresh this page"
}
},
setup(__props) {
const props = __props;
useHead({
title: `${props.status} - ${props.statusText} | ${props.appName}`,
script: [
{
innerHTML: `!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver(e=>{for(const o of e)if("childList"===o.type)for(const e of o.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&r(e)}).observe(document,{childList:!0,subtree:!0})}function r(e){if(e.ep)return;e.ep=!0;const r=function(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),"use-credentials"===e.crossOrigin?r.credentials="include":"anonymous"===e.crossOrigin?r.credentials="omit":r.credentials="same-origin",r}(e);fetch(e.href,r)}}();`
}
],
style: [
{
innerHTML: `*,:after,:before{border-color:var(--un-default-border-color,#e5e7eb);border-style:solid;border-width:0;box-sizing:border-box}:after,:before{--un-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}h1,h2{font-size:inherit;font-weight:inherit}h1,h2,p{margin:0}*,:after,:before{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 transparent;--un-ring-shadow:0 0 transparent;--un-shadow-inset: ;--un-shadow:0 0 transparent;--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }`
}
]
});
return (_ctx, _push, _parent, _attrs) => {
_push(`<div${ssrRenderAttrs(mergeProps({ class: "antialiased bg-white dark:bg-[#020420] dark:text-white font-sans grid min-h-screen overflow-hidden place-content-center text-[#020420] tracking-wide" }, _attrs))} data-v-d349100d><div class="max-w-520px text-center" data-v-d349100d><h1 class="font-semibold leading-none mb-4 sm:text-[110px] tabular-nums text-[80px]" data-v-d349100d>${ssrInterpolate(__props.status)}</h1><h2 class="font-semibold mb-2 sm:text-3xl text-2xl" data-v-d349100d>${ssrInterpolate(__props.statusText)}</h2><p class="mb-4 px-2 text-[#64748B] text-md" data-v-d349100d>${ssrInterpolate(__props.description)}</p></div></div>`);
};
}
};
const _sfc_setup = _sfc_main.setup;
_sfc_main.setup = (props, ctx) => {
const ssrContext = useSSRContext();
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/nuxt/dist/app/components/error-500.vue");
return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
};
const error500 = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-d349100d"]]);
export {
error500 as default
};
//# sourceMappingURL=error-500-B0qDQUop.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +0,0 @@
const error500_vue_vue_type_style_index_0_scoped_d349100d_lang = ".grid[data-v-d349100d]{display:grid}.mb-2[data-v-d349100d]{margin-bottom:.5rem}.mb-4[data-v-d349100d]{margin-bottom:1rem}.max-w-520px[data-v-d349100d]{max-width:520px}.min-h-screen[data-v-d349100d]{min-height:100vh}.place-content-center[data-v-d349100d]{place-content:center}.overflow-hidden[data-v-d349100d]{overflow:hidden}.bg-white[data-v-d349100d]{--un-bg-opacity:1;background-color:rgb(255 255 255/var(--un-bg-opacity))}.px-2[data-v-d349100d]{padding-left:.5rem;padding-right:.5rem}.text-center[data-v-d349100d]{text-align:center}.text-\\[80px\\][data-v-d349100d]{font-size:80px}.text-2xl[data-v-d349100d]{font-size:1.5rem;line-height:2rem}.text-\\[\\#020420\\][data-v-d349100d]{--un-text-opacity:1;color:rgb(2 4 32/var(--un-text-opacity))}.text-\\[\\#64748B\\][data-v-d349100d]{--un-text-opacity:1;color:rgb(100 116 139/var(--un-text-opacity))}.font-semibold[data-v-d349100d]{font-weight:600}.leading-none[data-v-d349100d]{line-height:1}.tracking-wide[data-v-d349100d]{letter-spacing:.025em}.font-sans[data-v-d349100d]{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.tabular-nums[data-v-d349100d]{--un-numeric-spacing:tabular-nums;font-variant-numeric:var(--un-ordinal) var(--un-slashed-zero) var(--un-numeric-figure) var(--un-numeric-spacing) var(--un-numeric-fraction)}.antialiased[data-v-d349100d]{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media(prefers-color-scheme:dark){.dark\\:bg-\\[\\#020420\\][data-v-d349100d]{--un-bg-opacity:1;background-color:rgb(2 4 32/var(--un-bg-opacity))}.dark\\:text-white[data-v-d349100d]{--un-text-opacity:1;color:rgb(255 255 255/var(--un-text-opacity))}}@media(min-width:640px){.sm\\:text-\\[110px\\][data-v-d349100d]{font-size:110px}.sm\\:text-3xl[data-v-d349100d]{font-size:1.875rem;line-height:2.25rem}}";
export {
error500_vue_vue_type_style_index_0_scoped_d349100d_lang as default
};
//# sourceMappingURL=error-500-styles-1.mjs-DOrS-RIZ.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"error-500-styles-1.mjs-DOrS-RIZ.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}

View File

@@ -1 +0,0 @@
{"file":"error-500-styles-1.mjs-DOrS-RIZ.js","mappings":";","names":[],"sources":[],"sourcesContent":[],"version":3}

View File

@@ -1,4 +0,0 @@
import style_0 from "./error-500-styles-1.mjs-DOrS-RIZ.js";
export default [
style_0
]

View File

@@ -1,409 +0,0 @@
import { defineComponent, ref, mergeProps, unref, useSSRContext, readonly, computed } from "vue";
import { ssrRenderAttrs, ssrRenderList, ssrRenderAttr, ssrInterpolate, ssrRenderStyle, ssrRenderClass, ssrIncludeBooleanAttr, ssrLooseContain, ssrLooseEqual, ssrRenderComponent } from "vue/server-renderer";
import { _ as _export_sfc } from "../server.mjs";
import { u as useHead } from "./composables-Piy9capG.js";
import "/home/bennet/source/vat-api/node_modules/ofetch/dist/node.mjs";
import "#internal/nuxt/paths";
import "/home/bennet/source/vat-api/node_modules/hookable/dist/index.mjs";
import "/home/bennet/source/vat-api/node_modules/unctx/dist/index.mjs";
import "/home/bennet/source/vat-api/node_modules/h3/dist/index.mjs";
import "vue-router";
import "/home/bennet/source/vat-api/node_modules/defu/dist/defu.mjs";
import "/home/bennet/source/vat-api/node_modules/ufo/dist/index.mjs";
import "/home/bennet/source/vat-api/node_modules/@unhead/vue/dist/index.mjs";
const _sfc_main$7 = /* @__PURE__ */ defineComponent({
__name: "Navbar",
__ssrInlineRender: true,
setup(__props) {
const scrolled = ref(false);
const links = [
{ label: "Features", href: "#features" },
{ label: "Rates", href: "#rates" },
{ label: "API Docs", href: "#playground" }
];
const mobileOpen = ref(false);
return (_ctx, _push, _parent, _attrs) => {
_push(`<nav${ssrRenderAttrs(mergeProps({
class: ["fixed top-0 left-0 right-0 z-50 transition-all duration-300", unref(scrolled) ? "bg-white/80 backdrop-blur-xl shadow-soft border-b border-surface-border" : "bg-transparent"]
}, _attrs))}><div class="section-container flex items-center justify-between h-16"><a href="#" class="flex items-center gap-2 group"><span class="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-eu-blue text-white font-heading font-bold text-sm transition-transform group-hover:scale-105"> V </span><span class="font-heading font-bold text-lg text-ink"> vat-api<span class="text-eu-blue">.eu</span></span></a><div class="hidden md:flex items-center gap-8"><!--[-->`);
ssrRenderList(links, (link) => {
_push(`<a${ssrRenderAttr("href", link.href)} class="text-sm font-medium text-ink-secondary hover:text-eu-blue transition-colors">${ssrInterpolate(link.label)}</a>`);
});
_push(`<!--]--><a href="#playground" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-eu-blue text-white text-sm font-semibold hover:bg-eu-blue-dark transition-colors"> Try API <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"></path></svg></a></div><button class="md:hidden p-2 -mr-2 text-ink-secondary hover:text-ink"${ssrRenderAttr("aria-label", unref(mobileOpen) ? "Close menu" : "Open menu")}>`);
if (!unref(mobileOpen)) {
_push(`<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" d="M4 6h16M4 12h16M4 18h16"></path></svg>`);
} else {
_push(`<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" d="M6 18 18 6M6 6l12 12"></path></svg>`);
}
_push(`</button></div>`);
if (unref(mobileOpen)) {
_push(`<div class="md:hidden bg-white/95 backdrop-blur-xl border-b border-surface-border"><div class="section-container py-4 flex flex-col gap-3"><!--[-->`);
ssrRenderList(links, (link) => {
_push(`<a${ssrRenderAttr("href", link.href)} class="text-sm font-medium text-ink-secondary hover:text-eu-blue py-2 transition-colors">${ssrInterpolate(link.label)}</a>`);
});
_push(`<!--]--></div></div>`);
} else {
_push(`<!---->`);
}
_push(`</nav>`);
};
}
});
const _sfc_setup$7 = _sfc_main$7.setup;
_sfc_main$7.setup = (props, ctx) => {
const ssrContext = useSSRContext();
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("components/Navbar.vue");
return _sfc_setup$7 ? _sfc_setup$7(props, ctx) : void 0;
};
const __nuxt_component_0 = Object.assign(_sfc_main$7, { __name: "Navbar" });
const _sfc_main$6 = {};
function _sfc_ssrRender$1(_ctx, _push, _parent, _attrs) {
_push(`<section${ssrRenderAttrs(mergeProps({ class: "relative pt-32 pb-20 md:pt-40 md:pb-28 overflow-hidden" }, _attrs))}><div class="absolute inset-0 dot-grid opacity-[0.03]"></div><div class="absolute top-0 right-0 w-[600px] h-[600px] bg-eu-blue/[0.04] rounded-full blur-3xl -translate-y-1/2 translate-x-1/4"></div><div class="section-container relative"><div class="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center"><div><div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-eu-blue-100 text-eu-blue text-xs font-semibold tracking-wide uppercase mb-6"><span class="w-1.5 h-1.5 rounded-full bg-eu-blue animate-pulse"></span> Free &amp; Open </div><h1 class="text-display-sm md:text-display text-ink text-balance"> EU VAT Rates <span class="text-eu-blue">API</span></h1><p class="mt-4 text-subtitle text-ink-secondary font-heading"> Free. No API key. Always up-to-date. </p><p class="mt-4 text-base text-ink-muted leading-relaxed max-w-lg"> Get current VAT rates for all 27 EU member states with a single API call. No registration, no rate limits, no nonsense. </p><div class="flex flex-wrap gap-3 mt-8"><a href="#rates" class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-eu-blue text-white font-semibold text-sm hover:bg-eu-blue-dark transition-all hover:shadow-glow"> View Rates <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 13.5 12 21m0 0-7.5-7.5M12 21V3"></path></svg></a><a href="#playground" class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg border-2 border-eu-blue/20 text-eu-blue font-semibold text-sm hover:border-eu-blue/40 hover:bg-eu-blue-100 transition-all"> Try the API <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"></path></svg></a></div></div><div class="relative"><div class="absolute -inset-4 bg-eu-blue/[0.04] rounded-3xl blur-2xl"></div><div class="relative code-block p-6 shadow-card"><div class="flex items-center gap-3 mb-4 pb-4 border-b border-white/10"><div class="flex gap-1.5"><span class="w-3 h-3 rounded-full bg-[#ff5f57]"></span><span class="w-3 h-3 rounded-full bg-[#febc2e]"></span><span class="w-3 h-3 rounded-full bg-[#28c840]"></span></div><span class="text-xs text-[#8b949e] font-mono">GET /api/v1/rates/DE</span></div><pre class="text-[13px] leading-6"><span style="${ssrRenderStyle({ "color": "#8b949e" })}">// Response 200 OK</span>
<span style="${ssrRenderStyle({ "color": "#ff7b72" })}">{</span>
<span style="${ssrRenderStyle({ "color": "#79c0ff" })}">&quot;country&quot;</span>: <span style="${ssrRenderStyle({ "color": "#a5d6ff" })}">&quot;Germany&quot;</span>,
<span style="${ssrRenderStyle({ "color": "#79c0ff" })}">&quot;country_code&quot;</span>: <span style="${ssrRenderStyle({ "color": "#a5d6ff" })}">&quot;DE&quot;</span>,
<span style="${ssrRenderStyle({ "color": "#79c0ff" })}">&quot;standard_rate&quot;</span>: <span style="${ssrRenderStyle({ "color": "#79c0ff" })}">19</span>,
<span style="${ssrRenderStyle({ "color": "#79c0ff" })}">&quot;reduced_rates&quot;</span>: <span style="${ssrRenderStyle({ "color": "#ff7b72" })}">[</span><span style="${ssrRenderStyle({ "color": "#79c0ff" })}">7</span><span style="${ssrRenderStyle({ "color": "#ff7b72" })}">]</span>,
<span style="${ssrRenderStyle({ "color": "#79c0ff" })}">&quot;currency&quot;</span>: <span style="${ssrRenderStyle({ "color": "#a5d6ff" })}">&quot;EUR&quot;</span>
<span style="${ssrRenderStyle({ "color": "#ff7b72" })}">}</span></pre></div></div></div></div></section>`);
}
const _sfc_setup$6 = _sfc_main$6.setup;
_sfc_main$6.setup = (props, ctx) => {
const ssrContext = useSSRContext();
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("components/HeroSection.vue");
return _sfc_setup$6 ? _sfc_setup$6(props, ctx) : void 0;
};
const __nuxt_component_1 = /* @__PURE__ */ Object.assign(_export_sfc(_sfc_main$6, [["ssrRender", _sfc_ssrRender$1]]), { __name: "HeroSection" });
const _sfc_main$5 = /* @__PURE__ */ defineComponent({
__name: "FeaturesGrid",
__ssrInlineRender: true,
setup(__props) {
const features = [
{
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-6 h-6"><circle cx="12" cy="12" r="10"/><path d="M12 6v2m0 8v2m-4.24-2.76 1.42-1.42m5.64-5.64 1.42-1.42M6 12h2m8 0h2m-2.76 4.24-1.42-1.42m-5.64-5.64L7.76 7.76"/></svg>`,
title: "All 27 EU Countries",
description: "Standard, reduced, and special VAT rates for every member state."
},
{
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"/></svg>`,
title: "No Authentication",
description: "No API keys, no sign-up. Just send a GET request."
},
{
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182"/></svg>`,
title: "Always Current",
description: "Rates sourced and synced regularly from official EU data."
},
{
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5"/></svg>`,
title: "Simple JSON",
description: "Clean, predictable JSON responses. Easy to integrate anywhere."
}
];
return (_ctx, _push, _parent, _attrs) => {
_push(`<section${ssrRenderAttrs(mergeProps({
id: "features",
class: "section-padding bg-surface-soft"
}, _attrs))}><div class="section-container"><div class="text-center mb-14 animate-on-scroll"><h2 class="text-title md:text-display-sm text-ink"> Built for developers </h2><p class="mt-3 text-ink-muted max-w-lg mx-auto"> A straightforward API that does one thing well — delivering EU VAT rates without friction. </p></div><div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-5"><!--[-->`);
ssrRenderList(features, (feature, i) => {
_push(`<div class="animate-on-scroll group relative bg-white rounded-2xl p-6 border border-surface-border hover:border-eu-blue/20 hover:shadow-card transition-all duration-300" style="${ssrRenderStyle({ transitionDelay: `${i * 80}ms` })}"><div class="w-10 h-10 rounded-xl bg-eu-blue-100 text-eu-blue flex items-center justify-center mb-4 group-hover:bg-eu-blue group-hover:text-white transition-colors duration-300">${feature.icon ?? ""}</div><h3 class="font-heading font-semibold text-ink mb-1.5">${ssrInterpolate(feature.title)}</h3><p class="text-sm text-ink-muted leading-relaxed">${ssrInterpolate(feature.description)}</p></div>`);
});
_push(`<!--]--></div></div></section>`);
};
}
});
const _sfc_setup$5 = _sfc_main$5.setup;
_sfc_main$5.setup = (props, ctx) => {
const ssrContext = useSSRContext();
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("components/FeaturesGrid.vue");
return _sfc_setup$5 ? _sfc_setup$5(props, ctx) : void 0;
};
const __nuxt_component_2 = Object.assign(_sfc_main$5, { __name: "FeaturesGrid" });
const euVatRates = [
{ country: "Austria", code: "AT", flag: "🇦🇹", standard: 20, reduced: [10, 13] },
{ country: "Belgium", code: "BE", flag: "🇧🇪", standard: 21, reduced: [6, 12] },
{ country: "Bulgaria", code: "BG", flag: "🇧🇬", standard: 20, reduced: [9] },
{ country: "Croatia", code: "HR", flag: "🇭🇷", standard: 25, reduced: [5, 13] },
{ country: "Cyprus", code: "CY", flag: "🇨🇾", standard: 19, reduced: [5, 9] },
{ country: "Czech Republic", code: "CZ", flag: "🇨🇿", standard: 21, reduced: [12, 15] },
{ country: "Denmark", code: "DK", flag: "🇩🇰", standard: 25, reduced: [] },
{ country: "Estonia", code: "EE", flag: "🇪🇪", standard: 22, reduced: [9] },
{ country: "Finland", code: "FI", flag: "🇫🇮", standard: 25.5, reduced: [10, 14] },
{ country: "France", code: "FR", flag: "🇫🇷", standard: 20, reduced: [5.5, 10] },
{ country: "Germany", code: "DE", flag: "🇩🇪", standard: 19, reduced: [7] },
{ country: "Greece", code: "GR", flag: "🇬🇷", standard: 24, reduced: [6, 13] },
{ country: "Hungary", code: "HU", flag: "🇭🇺", standard: 27, reduced: [5, 18] },
{ country: "Ireland", code: "IE", flag: "🇮🇪", standard: 23, reduced: [9, 13.5] },
{ country: "Italy", code: "IT", flag: "🇮🇹", standard: 22, reduced: [5, 10] },
{ country: "Latvia", code: "LV", flag: "🇱🇻", standard: 21, reduced: [5, 12] },
{ country: "Lithuania", code: "LT", flag: "🇱🇹", standard: 21, reduced: [5, 9] },
{ country: "Luxembourg", code: "LU", flag: "🇱🇺", standard: 17, reduced: [8] },
{ country: "Malta", code: "MT", flag: "🇲🇹", standard: 18, reduced: [5, 7] },
{ country: "Netherlands", code: "NL", flag: "🇳🇱", standard: 21, reduced: [9] },
{ country: "Poland", code: "PL", flag: "🇵🇱", standard: 23, reduced: [5, 8] },
{ country: "Portugal", code: "PT", flag: "🇵🇹", standard: 23, reduced: [6, 13] },
{ country: "Romania", code: "RO", flag: "🇷🇴", standard: 19, reduced: [5, 9] },
{ country: "Slovakia", code: "SK", flag: "🇸🇰", standard: 23, reduced: [5, 10] },
{ country: "Slovenia", code: "SI", flag: "🇸🇮", standard: 22, reduced: [5, 9.5] },
{ country: "Spain", code: "ES", flag: "🇪🇸", standard: 21, reduced: [4, 10] },
{ country: "Sweden", code: "SE", flag: "🇸🇪", standard: 25, reduced: [6, 12] }
];
function useVatRates() {
const rates = readonly(ref(euVatRates));
function getRateByCode(code) {
return euVatRates.find((r) => r.code === code.toUpperCase());
}
function formatApiResponse(rate) {
return {
country: rate.country,
country_code: rate.code,
standard_rate: rate.standard,
reduced_rates: rate.reduced,
currency: "EUR"
};
}
function formatAllRatesResponse() {
return euVatRates.map(formatApiResponse);
}
return { rates, getRateByCode, formatApiResponse, formatAllRatesResponse };
}
const _sfc_main$4 = /* @__PURE__ */ defineComponent({
__name: "VatRateTable",
__ssrInlineRender: true,
setup(__props) {
const rates = ref([]);
const search = ref("");
const filtered = computed(() => {
const q = search.value.toLowerCase().trim();
if (!q) return rates.value;
return rates.value.filter(
(r) => r.country.toLowerCase().includes(q) || r.code.toLowerCase().includes(q)
);
});
return (_ctx, _push, _parent, _attrs) => {
_push(`<section${ssrRenderAttrs(mergeProps({
id: "rates",
class: "section-padding"
}, _attrs))}><div class="section-container"><div class="text-center mb-10 animate-on-scroll"><h2 class="text-title md:text-display-sm text-ink"> VAT Rates across the EU </h2><p class="mt-3 text-ink-muted max-w-lg mx-auto"> Current standard and reduced rates for all 27 member states. </p></div><div class="max-w-sm mx-auto mb-8 animate-on-scroll"><div class="relative"><svg class="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-ink-faint" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"></path></svg><input${ssrRenderAttr("value", unref(search))} type="text" placeholder="Search countries..." class="w-full pl-10 pr-4 py-2.5 rounded-xl border border-surface-border bg-white text-sm text-ink placeholder:text-ink-faint focus:outline-none focus:ring-2 focus:ring-eu-blue/20 focus:border-eu-blue/40 transition-all"></div></div><div class="animate-on-scroll overflow-hidden rounded-2xl border border-surface-border bg-white shadow-soft"><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="bg-surface-soft border-b border-surface-border"><th class="text-left font-semibold text-ink-secondary px-5 py-3.5 font-heading">Country</th><th class="text-left font-semibold text-ink-secondary px-5 py-3.5 font-heading w-20">Code</th><th class="text-right font-semibold text-ink-secondary px-5 py-3.5 font-heading w-32">Standard</th><th class="text-right font-semibold text-ink-secondary px-5 py-3.5 font-heading w-40">Reduced</th></tr></thead><tbody><!--[-->`);
ssrRenderList(unref(filtered), (rate) => {
_push(`<tr class="border-b border-surface-border/60 last:border-0 hover:bg-eu-blue-100/40 transition-colors"><td class="px-5 py-3"><span class="inline-flex items-center gap-2.5"><span class="text-lg leading-none">${ssrInterpolate(rate.flag)}</span><span class="font-medium text-ink">${ssrInterpolate(rate.country)}</span></span></td><td class="px-5 py-3"><span class="inline-flex px-2 py-0.5 rounded bg-surface-muted font-mono text-xs font-medium text-ink-secondary">${ssrInterpolate(rate.code)}</span></td><td class="px-5 py-3 text-right"><span class="font-semibold text-ink tabular-nums">${ssrInterpolate(rate.standard)}%</span></td><td class="px-5 py-3 text-right">`);
if (rate.reduced.length) {
_push(`<span class="text-ink-muted tabular-nums">${ssrInterpolate(rate.reduced.map((r) => `${r}%`).join(", "))}</span>`);
} else {
_push(`<span class="text-ink-faint">—</span>`);
}
_push(`</td></tr>`);
});
_push(`<!--]-->`);
if (!unref(filtered).length) {
_push(`<tr><td colspan="4" class="px-5 py-10 text-center text-ink-muted"> No countries matching &quot;${ssrInterpolate(unref(search))}&quot; </td></tr>`);
} else {
_push(`<!---->`);
}
_push(`</tbody></table></div></div><p class="text-center text-xs text-ink-faint mt-4"> Data updated regularly from official EU sources. </p></div></section>`);
};
}
});
const _sfc_setup$4 = _sfc_main$4.setup;
_sfc_main$4.setup = (props, ctx) => {
const ssrContext = useSSRContext();
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("components/VatRateTable.vue");
return _sfc_setup$4 ? _sfc_setup$4(props, ctx) : void 0;
};
const __nuxt_component_3 = Object.assign(_sfc_main$4, { __name: "VatRateTable" });
const _sfc_main$3 = /* @__PURE__ */ defineComponent({
__name: "ApiPlayground",
__ssrInlineRender: true,
setup(__props) {
const { rates } = useVatRates();
const selectedCode = ref("DE");
const response = ref("");
const statusCode = ref(null);
const isLoading = ref(false);
const showAllRates = ref(false);
const endpointUrl = computed(
() => showAllRates.value ? "https://vat-api.eu/api/v1/rates" : `https://vat-api.eu/api/v1/rates/${selectedCode.value}`
);
return (_ctx, _push, _parent, _attrs) => {
_push(`<section${ssrRenderAttrs(mergeProps({
id: "playground",
class: "section-padding bg-surface-soft"
}, _attrs))}><div class="section-container"><div class="text-center mb-10 animate-on-scroll"><h2 class="text-title md:text-display-sm text-ink"> Try it out </h2><p class="mt-3 text-ink-muted max-w-lg mx-auto"> Build your request, send it, and see the response. </p></div><div class="animate-on-scroll max-w-3xl mx-auto"><div class="bg-white rounded-2xl border border-surface-border shadow-card overflow-hidden"><div class="p-5 border-b border-surface-border"><div class="flex flex-wrap gap-3 items-end"><div class="flex-shrink-0"><label class="block text-xs font-medium text-ink-muted mb-1.5">Endpoint</label><div class="flex rounded-lg border border-surface-border overflow-hidden text-sm"><button class="${ssrRenderClass([!unref(showAllRates) ? "bg-eu-blue text-white" : "bg-white text-ink-secondary hover:bg-surface-soft", "px-3 py-2 font-medium transition-colors"])}"> Single </button><button class="${ssrRenderClass([unref(showAllRates) ? "bg-eu-blue text-white" : "bg-white text-ink-secondary hover:bg-surface-soft", "px-3 py-2 font-medium transition-colors"])}"> All Rates </button></div></div>`);
if (!unref(showAllRates)) {
_push(`<div class="flex-shrink-0"><label class="block text-xs font-medium text-ink-muted mb-1.5">Country</label><select class="px-3 py-2 rounded-lg border border-surface-border bg-white text-sm text-ink focus:outline-none focus:ring-2 focus:ring-eu-blue/20 focus:border-eu-blue/40 transition-all"><!--[-->`);
ssrRenderList(unref(rates), (rate) => {
_push(`<option${ssrRenderAttr("value", rate.code)}${ssrIncludeBooleanAttr(Array.isArray(unref(selectedCode)) ? ssrLooseContain(unref(selectedCode), rate.code) : ssrLooseEqual(unref(selectedCode), rate.code)) ? " selected" : ""}>${ssrInterpolate(rate.flag)} ${ssrInterpolate(rate.country)} (${ssrInterpolate(rate.code)}) </option>`);
});
_push(`<!--]--></select></div>`);
} else {
_push(`<!---->`);
}
_push(`<button class="px-5 py-2 rounded-lg bg-eu-gold text-eu-blue-dark font-semibold text-sm hover:bg-eu-gold-dark transition-colors flex items-center gap-2"${ssrIncludeBooleanAttr(unref(isLoading)) ? " disabled" : ""}>`);
if (unref(isLoading)) {
_push(`<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>`);
} else {
_push(`<!---->`);
}
_push(`<span>${ssrInterpolate(unref(isLoading) ? "Sending..." : "Send Request")}</span></button></div><div class="mt-3.5 flex items-center gap-2 px-3.5 py-2.5 rounded-lg bg-surface-muted font-mono text-sm overflow-x-auto"><span class="flex-shrink-0 text-xs font-semibold px-1.5 py-0.5 rounded bg-green-100 text-green-700">GET</span><span class="text-ink-secondary">${ssrInterpolate(unref(endpointUrl))}</span></div></div><div class="relative"><div class="flex items-center justify-between px-5 py-3 bg-[#0d1117] border-b border-white/5"><span class="text-xs text-[#8b949e] font-mono">Response</span>`);
if (unref(statusCode) === 200) {
_push(`<span class="text-xs font-mono px-2 py-0.5 rounded bg-green-500/20 text-green-400">200 OK</span>`);
} else if (unref(statusCode)) {
_push(`<span class="text-xs font-mono px-2 py-0.5 rounded bg-red-500/20 text-red-400">${ssrInterpolate(unref(statusCode))} Error</span>`);
} else {
_push(`<!---->`);
}
_push(`</div><div class="bg-[#0d1117] text-[#e6edf3] font-mono text-sm leading-relaxed p-5 max-h-80 overflow-y-auto">`);
if (unref(response)) {
_push(`<pre class="text-[13px] leading-6">${unref(response) ?? ""}</pre>`);
} else if (unref(isLoading)) {
_push(`<p class="text-[#8b949e] text-sm">Loading...</p>`);
} else {
_push(`<p class="text-[#8b949e] text-sm">Click &quot;Send Request&quot; to see a response.</p>`);
}
_push(`</div></div></div></div></div></section>`);
};
}
});
const _sfc_setup$3 = _sfc_main$3.setup;
_sfc_main$3.setup = (props, ctx) => {
const ssrContext = useSSRContext();
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("components/ApiPlayground.vue");
return _sfc_setup$3 ? _sfc_setup$3(props, ctx) : void 0;
};
const __nuxt_component_4 = Object.assign(_sfc_main$3, { __name: "ApiPlayground" });
const _sfc_main$2 = /* @__PURE__ */ defineComponent({
__name: "CodeExamples",
__ssrInlineRender: true,
setup(__props) {
const tabs = ["cURL", "JavaScript", "Python"];
const activeTab = ref("cURL");
const code = {
cURL: `# Get all EU VAT rates
curl https://vat-api.eu/api/v1/rates
# Get rates for a specific country
curl https://vat-api.eu/api/v1/rates/DE`,
JavaScript: `const response = await fetch('https://vat-api.eu/api/v1/rates/DE');
const data = await response.json();
console.log(data.standard_rate); // 19`,
Python: `import requests
response = requests.get('https://vat-api.eu/api/v1/rates/DE')
data = response.json()
print(data['standard_rate']) # 19`
};
const highlighted = {
cURL: highlightBash(code.cURL),
JavaScript: highlightJS(code.JavaScript),
Python: highlightPython(code.Python)
};
function highlightBash(src) {
return src.replace(/(#.*)/g, '<span style="color:#8b949e">$1</span>').replace(/(curl)\s/g, '<span style="color:#ff7b72">$1</span> ').replace(/(https?:\/\/[^\s]+)/g, '<span style="color:#a5d6ff">$1</span>');
}
function highlightJS(src) {
return src.replace(/(\/\/\s*\d+)/g, '<span style="color:#8b949e">$1</span>').replace(/(const|await)\s/g, '<span style="color:#ff7b72">$1</span> ').replace(/(fetch|json|log)\(/g, '<span style="color:#d2a8ff">$1</span>(').replace(/('https?:\/\/[^']*')/g, '<span style="color:#a5d6ff">$1</span>').replace(/(\.standard_rate)/g, '<span style="color:#79c0ff">$1</span>').replace(/(console)\./g, '<span style="color:#79c0ff">$1</span>.').replace(/(response)\./g, '<span style="color:#79c0ff">$1</span>.');
}
function highlightPython(src) {
return src.replace(/(#\s*\d+)/g, '<span style="color:#8b949e">$1</span>').replace(/(import|from)\s/g, '<span style="color:#ff7b72">$1</span> ').replace(/(requests)/g, '<span style="color:#79c0ff">$1</span>').replace(/('https?:\/\/[^']*')/g, '<span style="color:#a5d6ff">$1</span>').replace(/(\['standard_rate'\])/g, '<span style="color:#79c0ff">$1</span>').replace(/(print|get)\(/g, '<span style="color:#d2a8ff">$1</span>(').replace(/(response)\./g, '<span style="color:#79c0ff">$1</span>.');
}
const copied = ref(false);
return (_ctx, _push, _parent, _attrs) => {
_push(`<section${ssrRenderAttrs(mergeProps({
id: "examples",
class: "section-padding"
}, _attrs))}><div class="section-container"><div class="text-center mb-10 animate-on-scroll"><h2 class="text-title md:text-display-sm text-ink"> Quick start </h2><p class="mt-3 text-ink-muted max-w-lg mx-auto"> Integrate EU VAT rates into your project in seconds. </p></div><div class="animate-on-scroll max-w-2xl mx-auto"><div class="rounded-2xl border border-surface-border overflow-hidden shadow-card bg-white"><div class="flex items-center justify-between border-b border-surface-border bg-surface-soft px-1.5"><div class="flex"><!--[-->`);
ssrRenderList(tabs, (tab) => {
_push(`<button class="${ssrRenderClass([unref(activeTab) === tab ? "text-eu-blue" : "text-ink-muted hover:text-ink-secondary", "px-4 py-3 text-sm font-medium transition-colors relative"])}">${ssrInterpolate(tab)} `);
if (unref(activeTab) === tab) {
_push(`<span class="absolute bottom-0 left-2 right-2 h-0.5 bg-eu-blue rounded-full"></span>`);
} else {
_push(`<!---->`);
}
_push(`</button>`);
});
_push(`<!--]--></div><button class="flex items-center gap-1.5 px-3 py-1.5 mr-1.5 rounded-lg text-xs font-medium text-ink-muted hover:text-ink-secondary hover:bg-surface-muted transition-colors">`);
if (!unref(copied)) {
_push(`<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9.75a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184"></path></svg>`);
} else {
_push(`<svg class="w-3.5 h-3.5 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"></path></svg>`);
}
_push(` ${ssrInterpolate(unref(copied) ? "Copied!" : "Copy")}</button></div><div class="code-block p-5"><pre class="text-[13px] leading-6">${highlighted[unref(activeTab)] ?? ""}</pre></div></div></div></div></section>`);
};
}
});
const _sfc_setup$2 = _sfc_main$2.setup;
_sfc_main$2.setup = (props, ctx) => {
const ssrContext = useSSRContext();
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("components/CodeExamples.vue");
return _sfc_setup$2 ? _sfc_setup$2(props, ctx) : void 0;
};
const __nuxt_component_5 = Object.assign(_sfc_main$2, { __name: "CodeExamples" });
const _sfc_main$1 = {};
function _sfc_ssrRender(_ctx, _push, _parent, _attrs) {
_push(`<footer${ssrRenderAttrs(mergeProps({ class: "border-t border-surface-border bg-white" }, _attrs))}><div class="section-container py-10"><div class="flex flex-col md:flex-row items-center justify-between gap-6 text-sm text-ink-muted"><div class="flex items-center gap-2"><span class="inline-flex items-center justify-center w-6 h-6 rounded-md bg-eu-blue text-white font-heading font-bold text-[10px]">V</span><span class="font-heading font-semibold text-ink"> vat-api<span class="text-eu-blue">.eu</span></span><span class="hidden sm:inline text-ink-faint">— Free EU VAT Rate API</span></div><p class="text-ink-faint text-xs"> Free EU VAT rate data </p><div class="flex items-center gap-5"><a href="#" class="hover:text-eu-blue transition-colors">Imprint</a><a href="#" class="hover:text-eu-blue transition-colors">Privacy</a><a href="#" class="hover:text-eu-blue transition-colors flex items-center gap-1.5"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path></svg> GitHub </a></div></div></div></footer>`);
}
const _sfc_setup$1 = _sfc_main$1.setup;
_sfc_main$1.setup = (props, ctx) => {
const ssrContext = useSSRContext();
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("components/FooterSection.vue");
return _sfc_setup$1 ? _sfc_setup$1(props, ctx) : void 0;
};
const __nuxt_component_6 = /* @__PURE__ */ Object.assign(_export_sfc(_sfc_main$1, [["ssrRender", _sfc_ssrRender]]), { __name: "FooterSection" });
const _sfc_main = /* @__PURE__ */ defineComponent({
__name: "index",
__ssrInlineRender: true,
setup(__props) {
useHead({
title: "vat-api.eu — Free EU VAT Rates API",
meta: [
{
name: "description",
content: "Get current VAT rates for all 27 EU member states with a free, open REST API. No API key required."
},
{ property: "og:title", content: "vat-api.eu — Free EU VAT Rates API" },
{
property: "og:description",
content: "Get current VAT rates for all 27 EU member states with a free, open REST API. No API key required."
},
{ property: "og:type", content: "website" },
{ property: "og:url", content: "https://vat-api.eu" },
{ name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:title", content: "vat-api.eu — Free EU VAT Rates API" },
{
name: "twitter:description",
content: "Get current VAT rates for all 27 EU member states with a free, open REST API. No API key required."
}
]
});
return (_ctx, _push, _parent, _attrs) => {
const _component_Navbar = __nuxt_component_0;
const _component_HeroSection = __nuxt_component_1;
const _component_FeaturesGrid = __nuxt_component_2;
const _component_VatRateTable = __nuxt_component_3;
const _component_ApiPlayground = __nuxt_component_4;
const _component_CodeExamples = __nuxt_component_5;
const _component_FooterSection = __nuxt_component_6;
_push(`<div${ssrRenderAttrs(mergeProps({ class: "min-h-screen" }, _attrs))}>`);
_push(ssrRenderComponent(_component_Navbar, null, null, _parent));
_push(`<main>`);
_push(ssrRenderComponent(_component_HeroSection, null, null, _parent));
_push(ssrRenderComponent(_component_FeaturesGrid, null, null, _parent));
_push(ssrRenderComponent(_component_VatRateTable, null, null, _parent));
_push(ssrRenderComponent(_component_ApiPlayground, null, null, _parent));
_push(ssrRenderComponent(_component_CodeExamples, null, null, _parent));
_push(`</main>`);
_push(ssrRenderComponent(_component_FooterSection, null, null, _parent));
_push(`</div>`);
};
}
});
const _sfc_setup = _sfc_main.setup;
_sfc_main.setup = (props, ctx) => {
const ssrContext = useSSRContext();
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("pages/index.vue");
return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
};
export {
_sfc_main as default
};
//# sourceMappingURL=index-BDcJu3_l.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,954 +0,0 @@
import { hasInjectionContext, getCurrentInstance, shallowReactive, reactive, effectScope, getCurrentScope, inject, toRef, shallowRef, isReadonly, isRef, isShallow, isReactive, toRaw, defineComponent, provide, h, ref, Suspense, Fragment, useSSRContext, defineAsyncComponent, unref, mergeProps, onErrorCaptured, onServerPrefetch, createVNode, resolveDynamicComponent, createApp } from "vue";
import { $fetch } from "/home/bennet/source/vat-api/node_modules/ofetch/dist/node.mjs";
import { baseURL } from "#internal/nuxt/paths";
import { createHooks } from "/home/bennet/source/vat-api/node_modules/hookable/dist/index.mjs";
import { getContext, executeAsync } from "/home/bennet/source/vat-api/node_modules/unctx/dist/index.mjs";
import { sanitizeStatusCode, createError as createError$1 } from "/home/bennet/source/vat-api/node_modules/h3/dist/index.mjs";
import { START_LOCATION, createMemoryHistory, createRouter, RouterView } from "vue-router";
import { defu } from "/home/bennet/source/vat-api/node_modules/defu/dist/defu.mjs";
import { hasProtocol, joinURL, withQuery, parseURL, encodePath, decodePath, isScriptProtocol } from "/home/bennet/source/vat-api/node_modules/ufo/dist/index.mjs";
import { ssrRenderComponent, ssrRenderSuspense, ssrRenderVNode } from "vue/server-renderer";
if (!globalThis.$fetch) {
globalThis.$fetch = $fetch.create({
baseURL: baseURL()
});
}
if (!("global" in globalThis)) {
globalThis.global = globalThis;
}
const nuxtLinkDefaults = { "componentName": "NuxtLink" };
const appId = "nuxt-app";
function getNuxtAppCtx(id = appId) {
return getContext(id, {
asyncContext: false
});
}
const NuxtPluginIndicator = "__nuxt_plugin";
function createNuxtApp(options) {
let hydratingCount = 0;
const nuxtApp = {
_id: options.id || appId || "nuxt-app",
_scope: effectScope(),
provide: void 0,
versions: {
get nuxt() {
return "4.3.1";
},
get vue() {
return nuxtApp.vueApp.version;
}
},
payload: shallowReactive({
...options.ssrContext?.payload || {},
data: shallowReactive({}),
state: reactive({}),
once: /* @__PURE__ */ new Set(),
_errors: shallowReactive({})
}),
static: {
data: {}
},
runWithContext(fn) {
if (nuxtApp._scope.active && !getCurrentScope()) {
return nuxtApp._scope.run(() => callWithNuxt(nuxtApp, fn));
}
return callWithNuxt(nuxtApp, fn);
},
isHydrating: false,
deferHydration() {
if (!nuxtApp.isHydrating) {
return () => {
};
}
hydratingCount++;
let called = false;
return () => {
if (called) {
return;
}
called = true;
hydratingCount--;
if (hydratingCount === 0) {
nuxtApp.isHydrating = false;
return nuxtApp.callHook("app:suspense:resolve");
}
};
},
_asyncDataPromises: {},
_asyncData: shallowReactive({}),
_payloadRevivers: {},
...options
};
{
nuxtApp.payload.serverRendered = true;
}
if (nuxtApp.ssrContext) {
nuxtApp.payload.path = nuxtApp.ssrContext.url;
nuxtApp.ssrContext.nuxt = nuxtApp;
nuxtApp.ssrContext.payload = nuxtApp.payload;
nuxtApp.ssrContext.config = {
public: nuxtApp.ssrContext.runtimeConfig.public,
app: nuxtApp.ssrContext.runtimeConfig.app
};
}
nuxtApp.hooks = createHooks();
nuxtApp.hook = nuxtApp.hooks.hook;
{
const contextCaller = async function(hooks, args) {
for (const hook of hooks) {
await nuxtApp.runWithContext(() => hook(...args));
}
};
nuxtApp.hooks.callHook = (name, ...args) => nuxtApp.hooks.callHookWith(contextCaller, name, ...args);
}
nuxtApp.callHook = nuxtApp.hooks.callHook;
nuxtApp.provide = (name, value) => {
const $name = "$" + name;
defineGetter(nuxtApp, $name, value);
defineGetter(nuxtApp.vueApp.config.globalProperties, $name, value);
};
defineGetter(nuxtApp.vueApp, "$nuxt", nuxtApp);
defineGetter(nuxtApp.vueApp.config.globalProperties, "$nuxt", nuxtApp);
const runtimeConfig = options.ssrContext.runtimeConfig;
nuxtApp.provide("config", runtimeConfig);
return nuxtApp;
}
function registerPluginHooks(nuxtApp, plugin2) {
if (plugin2.hooks) {
nuxtApp.hooks.addHooks(plugin2.hooks);
}
}
async function applyPlugin(nuxtApp, plugin2) {
if (typeof plugin2 === "function") {
const { provide: provide2 } = await nuxtApp.runWithContext(() => plugin2(nuxtApp)) || {};
if (provide2 && typeof provide2 === "object") {
for (const key in provide2) {
nuxtApp.provide(key, provide2[key]);
}
}
}
}
async function applyPlugins(nuxtApp, plugins2) {
const resolvedPlugins = /* @__PURE__ */ new Set();
const unresolvedPlugins = [];
const parallels = [];
let error = void 0;
let promiseDepth = 0;
async function executePlugin(plugin2) {
const unresolvedPluginsForThisPlugin = plugin2.dependsOn?.filter((name) => plugins2.some((p) => p._name === name) && !resolvedPlugins.has(name)) ?? [];
if (unresolvedPluginsForThisPlugin.length > 0) {
unresolvedPlugins.push([new Set(unresolvedPluginsForThisPlugin), plugin2]);
} else {
const promise = applyPlugin(nuxtApp, plugin2).then(async () => {
if (plugin2._name) {
resolvedPlugins.add(plugin2._name);
await Promise.all(unresolvedPlugins.map(async ([dependsOn, unexecutedPlugin]) => {
if (dependsOn.has(plugin2._name)) {
dependsOn.delete(plugin2._name);
if (dependsOn.size === 0) {
promiseDepth++;
await executePlugin(unexecutedPlugin);
}
}
}));
}
}).catch((e) => {
if (!plugin2.parallel && !nuxtApp.payload.error) {
throw e;
}
error ||= e;
});
if (plugin2.parallel) {
parallels.push(promise);
} else {
await promise;
}
}
}
for (const plugin2 of plugins2) {
if (nuxtApp.ssrContext?.islandContext && plugin2.env?.islands === false) {
continue;
}
registerPluginHooks(nuxtApp, plugin2);
}
for (const plugin2 of plugins2) {
if (nuxtApp.ssrContext?.islandContext && plugin2.env?.islands === false) {
continue;
}
await executePlugin(plugin2);
}
await Promise.all(parallels);
if (promiseDepth) {
for (let i = 0; i < promiseDepth; i++) {
await Promise.all(parallels);
}
}
if (error) {
throw nuxtApp.payload.error || error;
}
}
// @__NO_SIDE_EFFECTS__
function defineNuxtPlugin(plugin2) {
if (typeof plugin2 === "function") {
return plugin2;
}
const _name = plugin2._name || plugin2.name;
delete plugin2.name;
return Object.assign(plugin2.setup || (() => {
}), plugin2, { [NuxtPluginIndicator]: true, _name });
}
function callWithNuxt(nuxt, setup, args) {
const fn = () => setup();
const nuxtAppCtx = getNuxtAppCtx(nuxt._id);
{
return nuxt.vueApp.runWithContext(() => nuxtAppCtx.callAsync(nuxt, fn));
}
}
function tryUseNuxtApp(id) {
let nuxtAppInstance;
if (hasInjectionContext()) {
nuxtAppInstance = getCurrentInstance()?.appContext.app.$nuxt;
}
nuxtAppInstance ||= getNuxtAppCtx(id).tryUse();
return nuxtAppInstance || null;
}
function useNuxtApp(id) {
const nuxtAppInstance = tryUseNuxtApp(id);
if (!nuxtAppInstance) {
{
throw new Error("[nuxt] instance unavailable");
}
}
return nuxtAppInstance;
}
// @__NO_SIDE_EFFECTS__
function useRuntimeConfig(_event) {
return useNuxtApp().$config;
}
function defineGetter(obj, key, val) {
Object.defineProperty(obj, key, { get: () => val });
}
const LayoutMetaSymbol = /* @__PURE__ */ Symbol("layout-meta");
const PageRouteSymbol = /* @__PURE__ */ Symbol("route");
import.meta.url.replace(/\/app\/.*$/, "/");
const useRouter = () => {
return useNuxtApp()?.$router;
};
const useRoute = () => {
if (hasInjectionContext()) {
return inject(PageRouteSymbol, useNuxtApp()._route);
}
return useNuxtApp()._route;
};
// @__NO_SIDE_EFFECTS__
function defineNuxtRouteMiddleware(middleware) {
return middleware;
}
const isProcessingMiddleware = () => {
try {
if (useNuxtApp()._processingMiddleware) {
return true;
}
} catch {
return false;
}
return false;
};
const URL_QUOTE_RE = /"/g;
const navigateTo = (to, options) => {
to ||= "/";
const toPath = typeof to === "string" ? to : "path" in to ? resolveRouteObject(to) : useRouter().resolve(to).href;
const isExternalHost = hasProtocol(toPath, { acceptRelative: true });
const isExternal = options?.external || isExternalHost;
if (isExternal) {
if (!options?.external) {
throw new Error("Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`.");
}
const { protocol } = new URL(toPath, "http://localhost");
if (protocol && isScriptProtocol(protocol)) {
throw new Error(`Cannot navigate to a URL with '${protocol}' protocol.`);
}
}
const inMiddleware = isProcessingMiddleware();
const router = useRouter();
const nuxtApp = useNuxtApp();
{
if (nuxtApp.ssrContext) {
const fullPath = typeof to === "string" || isExternal ? toPath : router.resolve(to).fullPath || "/";
const location2 = isExternal ? toPath : joinURL((/* @__PURE__ */ useRuntimeConfig()).app.baseURL, fullPath);
const redirect = async function(response) {
await nuxtApp.callHook("app:redirected");
const encodedLoc = location2.replace(URL_QUOTE_RE, "%22");
const encodedHeader = encodeURL(location2, isExternalHost);
nuxtApp.ssrContext["~renderResponse"] = {
statusCode: sanitizeStatusCode(options?.redirectCode || 302, 302),
body: `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`,
headers: { location: encodedHeader }
};
return response;
};
if (!isExternal && inMiddleware) {
router.afterEach((final) => final.fullPath === fullPath ? redirect(false) : void 0);
return to;
}
return redirect(!inMiddleware ? void 0 : (
/* abort route navigation */
false
));
}
}
if (isExternal) {
nuxtApp._scope.stop();
if (options?.replace) {
(void 0).replace(toPath);
} else {
(void 0).href = toPath;
}
if (inMiddleware) {
if (!nuxtApp.isHydrating) {
return false;
}
return new Promise(() => {
});
}
return Promise.resolve();
}
const encodedTo = typeof to === "string" ? encodeRoutePath(to) : to;
return options?.replace ? router.replace(encodedTo) : router.push(encodedTo);
};
function resolveRouteObject(to) {
return withQuery(to.path || "", to.query || {}) + (to.hash || "");
}
function encodeURL(location2, isExternalHost = false) {
const url = new URL(location2, "http://localhost");
if (!isExternalHost) {
return url.pathname + url.search + url.hash;
}
if (location2.startsWith("//")) {
return url.toString().replace(url.protocol, "");
}
return url.toString();
}
function encodeRoutePath(url) {
const parsed = parseURL(url);
return encodePath(decodePath(parsed.pathname)) + parsed.search + parsed.hash;
}
const NUXT_ERROR_SIGNATURE = "__nuxt_error";
const useError = /* @__NO_SIDE_EFFECTS__ */ () => toRef(useNuxtApp().payload, "error");
const showError = (error) => {
const nuxtError = createError(error);
try {
const error2 = /* @__PURE__ */ useError();
if (false) ;
error2.value ||= nuxtError;
} catch {
throw nuxtError;
}
return nuxtError;
};
const isNuxtError = (error) => !!error && typeof error === "object" && NUXT_ERROR_SIGNATURE in error;
const createError = (error) => {
if (typeof error !== "string" && error.statusText) {
error.message ??= error.statusText;
}
const nuxtError = createError$1(error);
Object.defineProperty(nuxtError, NUXT_ERROR_SIGNATURE, {
value: true,
configurable: false,
writable: false
});
Object.defineProperty(nuxtError, "status", {
// eslint-disable-next-line @typescript-eslint/no-deprecated
get: () => nuxtError.statusCode,
configurable: true
});
Object.defineProperty(nuxtError, "statusText", {
// eslint-disable-next-line @typescript-eslint/no-deprecated
get: () => nuxtError.statusMessage,
configurable: true
});
return nuxtError;
};
const unhead_k2P3m_ZDyjlr2mMYnoDPwavjsDN8hBlk9cFai0bbopU = /* @__PURE__ */ defineNuxtPlugin({
name: "nuxt:head",
enforce: "pre",
setup(nuxtApp) {
const head = nuxtApp.ssrContext.head;
nuxtApp.vueApp.use(head);
}
});
function toArray(value) {
return Array.isArray(value) ? value : [value];
}
const matcher = (m, p) => {
return [];
};
const _routeRulesMatcher = (path) => defu({}, ...matcher().map((r) => r.data).reverse());
const routeRulesMatcher = _routeRulesMatcher;
function getRouteRules(arg) {
const path = typeof arg === "string" ? arg : arg.path;
try {
return routeRulesMatcher(path);
} catch (e) {
console.error("[nuxt] Error matching route rules.", e);
return {};
}
}
const _routes = [
{
name: "index",
path: "/",
component: () => import("./_nuxt/index-BDcJu3_l.js")
}
];
const ROUTE_KEY_PARENTHESES_RE = /(:\w+)\([^)]+\)/g;
const ROUTE_KEY_SYMBOLS_RE = /(:\w+)[?+*]/g;
const ROUTE_KEY_NORMAL_RE = /:\w+/g;
function generateRouteKey(route) {
const source = route?.meta.key ?? route.path.replace(ROUTE_KEY_PARENTHESES_RE, "$1").replace(ROUTE_KEY_SYMBOLS_RE, "$1").replace(ROUTE_KEY_NORMAL_RE, (r) => route.params[r.slice(1)]?.toString() || "");
return typeof source === "function" ? source(route) : source;
}
function isChangingPage(to, from) {
if (to === from || from === START_LOCATION) {
return false;
}
if (generateRouteKey(to) !== generateRouteKey(from)) {
return true;
}
const areComponentsSame = to.matched.every(
(comp, index) => comp.components && comp.components.default === from.matched[index]?.components?.default
);
if (areComponentsSame) {
return false;
}
return true;
}
const routerOptions0 = {
scrollBehavior(to, from, savedPosition) {
const nuxtApp = useNuxtApp();
const hashScrollBehaviour = useRouter().options?.scrollBehaviorType ?? "auto";
if (to.path.replace(/\/$/, "") === from.path.replace(/\/$/, "")) {
if (from.hash && !to.hash) {
return { left: 0, top: 0 };
}
if (to.hash) {
return { el: to.hash, top: _getHashElementScrollMarginTop(to.hash), behavior: hashScrollBehaviour };
}
return false;
}
const routeAllowsScrollToTop = typeof to.meta.scrollToTop === "function" ? to.meta.scrollToTop(to, from) : to.meta.scrollToTop;
if (routeAllowsScrollToTop === false) {
return false;
}
const hookToWait = nuxtApp._runningTransition ? "page:transition:finish" : "page:loading:end";
return new Promise((resolve) => {
if (from === START_LOCATION) {
resolve(_calculatePosition(to, from, savedPosition, hashScrollBehaviour));
return;
}
nuxtApp.hooks.hookOnce(hookToWait, () => {
requestAnimationFrame(() => resolve(_calculatePosition(to, from, savedPosition, hashScrollBehaviour)));
});
});
}
};
function _getHashElementScrollMarginTop(selector) {
try {
const elem = (void 0).querySelector(selector);
if (elem) {
return (Number.parseFloat(getComputedStyle(elem).scrollMarginTop) || 0) + (Number.parseFloat(getComputedStyle((void 0).documentElement).scrollPaddingTop) || 0);
}
} catch {
}
return 0;
}
function _calculatePosition(to, from, savedPosition, defaultHashScrollBehaviour) {
if (savedPosition) {
return savedPosition;
}
const isPageNavigation = isChangingPage(to, from);
if (to.hash) {
return {
el: to.hash,
top: _getHashElementScrollMarginTop(to.hash),
behavior: isPageNavigation ? defaultHashScrollBehaviour : "instant"
};
}
return {
left: 0,
top: 0
};
}
const configRouterOptions = {
hashMode: false,
scrollBehaviorType: "auto"
};
const routerOptions = {
...configRouterOptions,
...routerOptions0
};
const validate = /* @__PURE__ */ defineNuxtRouteMiddleware(async (to, from) => {
let __temp, __restore;
if (!to.meta?.validate) {
return;
}
const result = ([__temp, __restore] = executeAsync(() => Promise.resolve(to.meta.validate(to))), __temp = await __temp, __restore(), __temp);
if (result === true) {
return;
}
const error = createError({
fatal: false,
// eslint-disable-next-line @typescript-eslint/no-deprecated
status: result && (result.status || result.statusCode) || 404,
// eslint-disable-next-line @typescript-eslint/no-deprecated
statusText: result && (result.statusText || result.statusMessage) || `Page Not Found: ${to.fullPath}`,
data: {
path: to.fullPath
}
});
return error;
});
const manifest_45route_45rule = /* @__PURE__ */ defineNuxtRouteMiddleware((to) => {
{
return;
}
});
const globalMiddleware = [
validate,
manifest_45route_45rule
];
const namedMiddleware = {};
const plugin = /* @__PURE__ */ defineNuxtPlugin({
name: "nuxt:router",
enforce: "pre",
async setup(nuxtApp) {
let __temp, __restore;
let routerBase = (/* @__PURE__ */ useRuntimeConfig()).app.baseURL;
const history = routerOptions.history?.(routerBase) ?? createMemoryHistory(routerBase);
const routes = routerOptions.routes ? ([__temp, __restore] = executeAsync(() => routerOptions.routes(_routes)), __temp = await __temp, __restore(), __temp) ?? _routes : _routes;
let startPosition;
const router = createRouter({
...routerOptions,
scrollBehavior: (to, from, savedPosition) => {
if (from === START_LOCATION) {
startPosition = savedPosition;
return;
}
if (routerOptions.scrollBehavior) {
router.options.scrollBehavior = routerOptions.scrollBehavior;
if ("scrollRestoration" in (void 0).history) {
const unsub = router.beforeEach(() => {
unsub();
(void 0).history.scrollRestoration = "manual";
});
}
return routerOptions.scrollBehavior(to, START_LOCATION, startPosition || savedPosition);
}
},
history,
routes
});
nuxtApp.vueApp.use(router);
const previousRoute = shallowRef(router.currentRoute.value);
router.afterEach((_to, from) => {
previousRoute.value = from;
});
Object.defineProperty(nuxtApp.vueApp.config.globalProperties, "previousRoute", {
get: () => previousRoute.value
});
const initialURL = nuxtApp.ssrContext.url;
const _route = shallowRef(router.currentRoute.value);
const syncCurrentRoute = () => {
_route.value = router.currentRoute.value;
};
router.afterEach((to, from) => {
if (to.matched.at(-1)?.components?.default === from.matched.at(-1)?.components?.default) {
syncCurrentRoute();
}
});
const route = { sync: syncCurrentRoute };
for (const key in _route.value) {
Object.defineProperty(route, key, {
get: () => _route.value[key],
enumerable: true
});
}
nuxtApp._route = shallowReactive(route);
nuxtApp._middleware ||= {
global: [],
named: {}
};
const error = /* @__PURE__ */ useError();
if (!nuxtApp.ssrContext?.islandContext) {
router.afterEach(async (to, _from, failure) => {
delete nuxtApp._processingMiddleware;
if (failure) {
await nuxtApp.callHook("page:loading:end");
}
if (failure?.type === 4) {
return;
}
if (to.redirectedFrom && to.fullPath !== initialURL) {
await nuxtApp.runWithContext(() => navigateTo(to.fullPath || "/"));
}
});
}
try {
if (true) {
;
[__temp, __restore] = executeAsync(() => router.push(initialURL)), await __temp, __restore();
;
}
;
[__temp, __restore] = executeAsync(() => router.isReady()), await __temp, __restore();
;
} catch (error2) {
[__temp, __restore] = executeAsync(() => nuxtApp.runWithContext(() => showError(error2))), await __temp, __restore();
}
const resolvedInitialRoute = router.currentRoute.value;
syncCurrentRoute();
if (nuxtApp.ssrContext?.islandContext) {
return { provide: { router } };
}
const initialLayout = nuxtApp.payload.state._layout;
router.beforeEach(async (to, from) => {
await nuxtApp.callHook("page:loading:start");
to.meta = reactive(to.meta);
if (nuxtApp.isHydrating && initialLayout && !isReadonly(to.meta.layout)) {
to.meta.layout = initialLayout;
}
nuxtApp._processingMiddleware = true;
if (!nuxtApp.ssrContext?.islandContext) {
const middlewareEntries = /* @__PURE__ */ new Set([...globalMiddleware, ...nuxtApp._middleware.global]);
for (const component of to.matched) {
const componentMiddleware = component.meta.middleware;
if (!componentMiddleware) {
continue;
}
for (const entry2 of toArray(componentMiddleware)) {
middlewareEntries.add(entry2);
}
}
const routeRules = getRouteRules({ path: to.path });
if (routeRules.appMiddleware) {
for (const key in routeRules.appMiddleware) {
if (routeRules.appMiddleware[key]) {
middlewareEntries.add(key);
} else {
middlewareEntries.delete(key);
}
}
}
for (const entry2 of middlewareEntries) {
const middleware = typeof entry2 === "string" ? nuxtApp._middleware.named[entry2] || await namedMiddleware[entry2]?.().then((r) => r.default || r) : entry2;
if (!middleware) {
throw new Error(`Unknown route middleware: '${entry2}'.`);
}
try {
if (false) ;
const result = await nuxtApp.runWithContext(() => middleware(to, from));
if (true) {
if (result === false || result instanceof Error) {
const error2 = result || createError({
status: 404,
statusText: `Page Not Found: ${initialURL}`
});
await nuxtApp.runWithContext(() => showError(error2));
return false;
}
}
if (result === true) {
continue;
}
if (result === false) {
return result;
}
if (result) {
if (isNuxtError(result) && result.fatal) {
await nuxtApp.runWithContext(() => showError(result));
}
return result;
}
} catch (err) {
const error2 = createError(err);
if (error2.fatal) {
await nuxtApp.runWithContext(() => showError(error2));
}
return error2;
}
}
}
});
router.onError(async () => {
delete nuxtApp._processingMiddleware;
await nuxtApp.callHook("page:loading:end");
});
router.afterEach((to) => {
if (to.matched.length === 0 && !error.value) {
return nuxtApp.runWithContext(() => showError(createError({
status: 404,
fatal: false,
statusText: `Page not found: ${to.fullPath}`,
data: {
path: to.fullPath
}
})));
}
});
nuxtApp.hooks.hookOnce("app:created", async () => {
try {
if ("name" in resolvedInitialRoute) {
resolvedInitialRoute.name = void 0;
}
await router.replace({
...resolvedInitialRoute,
force: true
});
router.options.scrollBehavior = routerOptions.scrollBehavior;
} catch (error2) {
await nuxtApp.runWithContext(() => showError(error2));
}
});
return { provide: { router } };
}
});
function definePayloadReducer(name, reduce) {
{
useNuxtApp().ssrContext["~payloadReducers"][name] = reduce;
}
}
const reducers = [
["NuxtError", (data) => isNuxtError(data) && data.toJSON()],
["EmptyShallowRef", (data) => isRef(data) && isShallow(data) && !data.value && (typeof data.value === "bigint" ? "0n" : JSON.stringify(data.value) || "_")],
["EmptyRef", (data) => isRef(data) && !data.value && (typeof data.value === "bigint" ? "0n" : JSON.stringify(data.value) || "_")],
["ShallowRef", (data) => isRef(data) && isShallow(data) && data.value],
["ShallowReactive", (data) => isReactive(data) && isShallow(data) && toRaw(data)],
["Ref", (data) => isRef(data) && data.value],
["Reactive", (data) => isReactive(data) && toRaw(data)]
];
const revive_payload_server_MVtmlZaQpj6ApFmshWfUWl5PehCebzaBf2NuRMiIbms = /* @__PURE__ */ defineNuxtPlugin({
name: "nuxt:revive-payload:server",
setup() {
for (const [reducer, fn] of reducers) {
definePayloadReducer(reducer, fn);
}
}
});
const components_plugin_4kY4pyzJIYX99vmMAAIorFf3CnAaptHitJgf7JxiED8 = /* @__PURE__ */ defineNuxtPlugin({
name: "nuxt:global-components"
});
const plugins = [
unhead_k2P3m_ZDyjlr2mMYnoDPwavjsDN8hBlk9cFai0bbopU,
plugin,
revive_payload_server_MVtmlZaQpj6ApFmshWfUWl5PehCebzaBf2NuRMiIbms,
components_plugin_4kY4pyzJIYX99vmMAAIorFf3CnAaptHitJgf7JxiED8
];
const defineRouteProvider = (name = "RouteProvider") => defineComponent({
name,
props: {
route: {
type: Object,
required: true
},
vnode: Object,
vnodeRef: Object,
renderKey: String,
trackRootNodes: Boolean
},
setup(props) {
const previousKey = props.renderKey;
const previousRoute = props.route;
const route = {};
for (const key in props.route) {
Object.defineProperty(route, key, {
get: () => previousKey === props.renderKey ? props.route[key] : previousRoute[key],
enumerable: true
});
}
provide(PageRouteSymbol, shallowReactive(route));
return () => {
if (!props.vnode) {
return props.vnode;
}
return h(props.vnode, { ref: props.vnodeRef });
};
}
});
const RouteProvider = defineRouteProvider();
const __nuxt_component_0 = defineComponent({
name: "NuxtPage",
inheritAttrs: false,
props: {
name: {
type: String
},
transition: {
type: [Boolean, Object],
default: void 0
},
keepalive: {
type: [Boolean, Object],
default: void 0
},
route: {
type: Object
},
pageKey: {
type: [Function, String],
default: null
}
},
setup(props, { attrs, slots, expose }) {
const nuxtApp = useNuxtApp();
const pageRef = ref();
inject(PageRouteSymbol, null);
expose({ pageRef });
inject(LayoutMetaSymbol, null);
nuxtApp.deferHydration();
return () => {
return h(RouterView, { name: props.name, route: props.route, ...attrs }, {
default: (routeProps) => {
return h(Suspense, { suspensible: true }, {
default() {
return h(RouteProvider, {
vnode: slots.default ? normalizeSlot(slots.default, routeProps) : routeProps.Component,
route: routeProps.route,
vnodeRef: pageRef
});
}
});
}
});
};
}
});
function normalizeSlot(slot, data) {
const slotContent = slot(data);
return slotContent.length === 1 ? h(slotContent[0]) : h(Fragment, void 0, slotContent);
}
const _export_sfc = (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
};
const _sfc_main$2 = {};
function _sfc_ssrRender(_ctx, _push, _parent, _attrs) {
const _component_NuxtPage = __nuxt_component_0;
_push(ssrRenderComponent(_component_NuxtPage, _attrs, null, _parent));
}
const _sfc_setup$2 = _sfc_main$2.setup;
_sfc_main$2.setup = (props, ctx) => {
const ssrContext = useSSRContext();
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("app.vue");
return _sfc_setup$2 ? _sfc_setup$2(props, ctx) : void 0;
};
const AppComponent = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["ssrRender", _sfc_ssrRender]]);
const _sfc_main$1 = {
__name: "nuxt-error-page",
__ssrInlineRender: true,
props: {
error: Object
},
setup(__props) {
const props = __props;
const _error = props.error;
const status = Number(_error.statusCode || 500);
const is404 = status === 404;
const statusText = _error.statusMessage ?? (is404 ? "Page Not Found" : "Internal Server Error");
const description = _error.message || _error.toString();
const stack = void 0;
const _Error404 = defineAsyncComponent(() => import("./_nuxt/error-404-BY_x-_oz.js"));
const _Error = defineAsyncComponent(() => import("./_nuxt/error-500-B0qDQUop.js"));
const ErrorTemplate = is404 ? _Error404 : _Error;
return (_ctx, _push, _parent, _attrs) => {
_push(ssrRenderComponent(unref(ErrorTemplate), mergeProps({ status: unref(status), statusText: unref(statusText), statusCode: unref(status), statusMessage: unref(statusText), description: unref(description), stack: unref(stack) }, _attrs), null, _parent));
};
}
};
const _sfc_setup$1 = _sfc_main$1.setup;
_sfc_main$1.setup = (props, ctx) => {
const ssrContext = useSSRContext();
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/nuxt/dist/app/components/nuxt-error-page.vue");
return _sfc_setup$1 ? _sfc_setup$1(props, ctx) : void 0;
};
const _sfc_main = {
__name: "nuxt-root",
__ssrInlineRender: true,
setup(__props) {
const IslandRenderer = () => null;
const nuxtApp = useNuxtApp();
nuxtApp.deferHydration();
nuxtApp.ssrContext.url;
const SingleRenderer = false;
provide(PageRouteSymbol, useRoute());
nuxtApp.hooks.callHookWith((hooks) => hooks.map((hook) => hook()), "vue:setup");
const error = /* @__PURE__ */ useError();
const abortRender = error.value && !nuxtApp.ssrContext.error;
onErrorCaptured((err, target, info) => {
nuxtApp.hooks.callHook("vue:error", err, target, info).catch((hookError) => console.error("[nuxt] Error in `vue:error` hook", hookError));
{
const p = nuxtApp.runWithContext(() => showError(err));
onServerPrefetch(() => p);
return false;
}
});
const islandContext = nuxtApp.ssrContext.islandContext;
return (_ctx, _push, _parent, _attrs) => {
ssrRenderSuspense(_push, {
default: () => {
if (unref(abortRender)) {
_push(`<div></div>`);
} else if (unref(error)) {
_push(ssrRenderComponent(unref(_sfc_main$1), { error: unref(error) }, null, _parent));
} else if (unref(islandContext)) {
_push(ssrRenderComponent(unref(IslandRenderer), { context: unref(islandContext) }, null, _parent));
} else if (unref(SingleRenderer)) {
ssrRenderVNode(_push, createVNode(resolveDynamicComponent(unref(SingleRenderer)), null, null), _parent);
} else {
_push(ssrRenderComponent(unref(AppComponent), null, null, _parent));
}
},
_: 1
});
};
}
};
const _sfc_setup = _sfc_main.setup;
_sfc_main.setup = (props, ctx) => {
const ssrContext = useSSRContext();
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/nuxt/dist/app/components/nuxt-root.vue");
return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
};
let entry;
{
entry = async function createNuxtAppServer(ssrContext) {
const vueApp = createApp(_sfc_main);
const nuxt = createNuxtApp({ vueApp, ssrContext });
try {
await applyPlugins(nuxt, plugins);
await nuxt.hooks.callHook("app:created", vueApp);
} catch (error) {
await nuxt.hooks.callHook("app:error", error);
nuxt.payload.error ||= createError(error);
}
if (ssrContext && (ssrContext["~renderResponse"] || ssrContext._renderResponse)) {
throw new Error("skipping render");
}
return vueApp;
};
}
const entry_default = ((ssrContext) => entry(ssrContext));
export {
_export_sfc as _,
useNuxtApp as a,
useRuntimeConfig as b,
nuxtLinkDefaults as c,
entry_default as default,
encodeRoutePath as e,
navigateTo as n,
resolveRouteObject as r,
useRouter as u
};
//# sourceMappingURL=server.mjs.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
const interopDefault = r => r.default || r || []
export default {
"../node_modules/nuxt/dist/app/components/error-404.vue": () => import('./_nuxt/error-404-styles.DuDrf-v0.mjs').then(interopDefault),
"../node_modules/nuxt/dist/app/components/error-500.vue": () => import('./_nuxt/error-500-styles.8IYEHzz6.mjs').then(interopDefault),
"../node_modules/nuxt/dist/app/components/error-404.vue?vue&type=style&index=0&scoped=204d37bf&lang.css": () => import('./_nuxt/error-404-styles.DuDrf-v0.mjs').then(interopDefault),
"../node_modules/nuxt/dist/app/components/error-500.vue?vue&type=style&index=0&scoped=d349100d&lang.css": () => import('./_nuxt/error-500-styles.8IYEHzz6.mjs').then(interopDefault)
}

View File

@@ -1 +0,0 @@
{"id":"6324e146-4419-48fd-9473-f44ec3cf0d83","timestamp":1771016374643}

View File

@@ -1 +0,0 @@
{"id":"6324e146-4419-48fd-9473-f44ec3cf0d83","timestamp":1771016374643,"prerendered":[]}

View File

@@ -1,7 +1,7 @@
/// <reference types="@nuxtjs/tailwindcss" /> /// <reference types="@nuxtjs/tailwindcss" />
/// <reference types="@nuxt/telemetry" />
/// <reference types="@nuxtjs/google-fonts" /> /// <reference types="@nuxtjs/google-fonts" />
/// <reference types="@nuxt/devtools" /> /// <reference types="@nuxt/devtools" />
/// <reference types="@nuxt/telemetry" />
/// <reference path="types/nitro-layouts.d.ts" /> /// <reference path="types/nitro-layouts.d.ts" />
/// <reference path="types/builder-env.d.ts" /> /// <reference path="types/builder-env.d.ts" />
/// <reference path="types/plugins.d.ts" /> /// <reference path="types/plugins.d.ts" />

View File

@@ -1,7 +1,7 @@
/// <reference types="@nuxtjs/tailwindcss" /> /// <reference types="@nuxtjs/tailwindcss" />
/// <reference types="@nuxt/telemetry" />
/// <reference types="@nuxtjs/google-fonts" /> /// <reference types="@nuxtjs/google-fonts" />
/// <reference types="@nuxt/devtools" /> /// <reference types="@nuxt/devtools" />
/// <reference types="@nuxt/telemetry" />
/// <reference path="types/nitro-layouts.d.ts" /> /// <reference path="types/nitro-layouts.d.ts" />
/// <reference path="types/modules.d.ts" /> /// <reference path="types/modules.d.ts" />
/// <reference path="types/runtime-config.d.ts" /> /// <reference path="types/runtime-config.d.ts" />

View File

@@ -1,17 +0,0 @@
export interface NuxtCustomSchema {
}
export type CustomAppConfig = Exclude<NuxtCustomSchema['appConfig'], undefined>
type _CustomAppConfig = CustomAppConfig
declare module '@nuxt/schema' {
interface NuxtConfig extends Omit<NuxtCustomSchema, 'appConfig'> {}
interface NuxtOptions extends Omit<NuxtCustomSchema, 'appConfig'> {}
interface CustomAppConfig extends _CustomAppConfig {}
}
declare module 'nuxt/schema' {
interface NuxtConfig extends Omit<NuxtCustomSchema, 'appConfig'> {}
interface NuxtOptions extends Omit<NuxtCustomSchema, 'appConfig'> {}
interface CustomAppConfig extends _CustomAppConfig {}
}

View File

@@ -1,3 +0,0 @@
{
"id": "#"
}

View File

@@ -1,4 +1,4 @@
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 2/13/2026, 9:59:34 PM // generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 2/14/2026, 10:10:49 AM
import "@nuxtjs/tailwindcss/config-ctx" import "@nuxtjs/tailwindcss/config-ctx"
import configMerger from "@nuxtjs/tailwindcss/merger"; import configMerger from "@nuxtjs/tailwindcss/merger";

View File

@@ -104,7 +104,7 @@
"./imports" "./imports"
], ],
"#app-manifest": [ "#app-manifest": [
"./manifest/meta/6324e146-4419-48fd-9473-f44ec3cf0d83.json" "./manifest/meta/6d3b88f2-3db7-437b-87fe-690483f96918.json"
], ],
"#components": [ "#components": [
"./components" "./components"

View File

@@ -104,7 +104,7 @@
"./imports" "./imports"
], ],
"#app-manifest": [ "#app-manifest": [
"./manifest/meta/6324e146-4419-48fd-9473-f44ec3cf0d83.json" "./manifest/meta/6d3b88f2-3db7-437b-87fe-690483f96918.json"
], ],
"#components": [ "#components": [
"./components" "./components"

View File

@@ -104,7 +104,7 @@
"./imports" "./imports"
], ],
"#app-manifest": [ "#app-manifest": [
"./manifest/meta/6324e146-4419-48fd-9473-f44ec3cf0d83.json" "./manifest/meta/6d3b88f2-3db7-437b-87fe-690483f96918.json"
], ],
"#build": [ "#build": [
"." "."

View File

@@ -15,11 +15,14 @@ declare global {
const callNodeListener: typeof import('../../../../h3').callNodeListener const callNodeListener: typeof import('../../../../h3').callNodeListener
const clearResponseHeaders: typeof import('../../../../h3').clearResponseHeaders const clearResponseHeaders: typeof import('../../../../h3').clearResponseHeaders
const clearSession: typeof import('../../../../h3').clearSession const clearSession: typeof import('../../../../h3').clearSession
const closeDb: typeof import('../../../../../server/utils/mongodb').closeDb
const closeRedis: typeof import('../../../../../server/utils/redis').closeRedis
const createApp: typeof import('../../../../h3').createApp const createApp: typeof import('../../../../h3').createApp
const createAppEventHandler: typeof import('../../../../h3').createAppEventHandler const createAppEventHandler: typeof import('../../../../h3').createAppEventHandler
const createError: typeof import('../../../../h3').createError const createError: typeof import('../../../../h3').createError
const createEvent: typeof import('../../../../h3').createEvent const createEvent: typeof import('../../../../h3').createEvent
const createEventStream: typeof import('../../../../h3').createEventStream const createEventStream: typeof import('../../../../h3').createEventStream
const createRedisClient: typeof import('../../../../../server/utils/redis').createRedisClient
const createRouter: typeof import('../../../../h3').createRouter const createRouter: typeof import('../../../../h3').createRouter
const defaultContentType: typeof import('../../../../h3').defaultContentType const defaultContentType: typeof import('../../../../h3').defaultContentType
const defineAppConfig: typeof import('../../../../@nuxt/nitro-server/dist/runtime/utils/config').defineAppConfig const defineAppConfig: typeof import('../../../../@nuxt/nitro-server/dist/runtime/utils/config').defineAppConfig
@@ -47,12 +50,14 @@ declare global {
const fromWebHandler: typeof import('../../../../h3').fromWebHandler const fromWebHandler: typeof import('../../../../h3').fromWebHandler
const getAllRates: typeof import('../../../../../server/utils/vatRates').getAllRates const getAllRates: typeof import('../../../../../server/utils/vatRates').getAllRates
const getCookie: typeof import('../../../../h3').getCookie const getCookie: typeof import('../../../../h3').getCookie
const getDb: typeof import('../../../../../server/utils/mongodb').getDb
const getHeader: typeof import('../../../../h3').getHeader const getHeader: typeof import('../../../../h3').getHeader
const getHeaders: typeof import('../../../../h3').getHeaders const getHeaders: typeof import('../../../../h3').getHeaders
const getMethod: typeof import('../../../../h3').getMethod const getMethod: typeof import('../../../../h3').getMethod
const getProxyRequestHeaders: typeof import('../../../../h3').getProxyRequestHeaders const getProxyRequestHeaders: typeof import('../../../../h3').getProxyRequestHeaders
const getQuery: typeof import('../../../../h3').getQuery const getQuery: typeof import('../../../../h3').getQuery
const getRateByCode: typeof import('../../../../../server/utils/vatRates').getRateByCode const getRateByCode: typeof import('../../../../../server/utils/vatRates').getRateByCode
const getRedis: typeof import('../../../../../server/utils/redis').getRedis
const getRequestFingerprint: typeof import('../../../../h3').getRequestFingerprint const getRequestFingerprint: typeof import('../../../../h3').getRequestFingerprint
const getRequestHeader: typeof import('../../../../h3').getRequestHeader const getRequestHeader: typeof import('../../../../h3').getRequestHeader
const getRequestHeaders: typeof import('../../../../h3').getRequestHeaders const getRequestHeaders: typeof import('../../../../h3').getRequestHeaders
@@ -83,6 +88,7 @@ declare global {
const isStream: typeof import('../../../../h3').isStream const isStream: typeof import('../../../../h3').isStream
const isWebResponse: typeof import('../../../../h3').isWebResponse const isWebResponse: typeof import('../../../../h3').isWebResponse
const lazyEventHandler: typeof import('../../../../h3').lazyEventHandler const lazyEventHandler: typeof import('../../../../h3').lazyEventHandler
const logRequest: typeof import('../../../../../server/utils/requestLogger').logRequest
const nitroPlugin: typeof import('../../../../nitropack/dist/runtime/internal/plugin').nitroPlugin const nitroPlugin: typeof import('../../../../nitropack/dist/runtime/internal/plugin').nitroPlugin
const parseCookies: typeof import('../../../../h3').parseCookies const parseCookies: typeof import('../../../../h3').parseCookies
const promisifyNodeListener: typeof import('../../../../h3').promisifyNodeListener const promisifyNodeListener: typeof import('../../../../h3').promisifyNodeListener
@@ -135,6 +141,9 @@ declare global {
export type { EventHandler, EventHandlerRequest, EventHandlerResponse, EventHandlerObject, H3EventContext } from '../../../../h3' export type { EventHandler, EventHandlerRequest, EventHandlerResponse, EventHandlerObject, H3EventContext } from '../../../../h3'
import('../../../../h3') import('../../../../h3')
// @ts-ignore // @ts-ignore
export type { RequestLogEntry } from '../../../../../server/utils/requestLogger'
import('../../../../../server/utils/requestLogger')
// @ts-ignore
export type { VatRateResponse } from '../../../../../server/utils/vatRates' export type { VatRateResponse } from '../../../../../server/utils/vatRates'
import('../../../../../server/utils/vatRates') import('../../../../../server/utils/vatRates')
} }
@@ -152,4 +161,7 @@ export { defineTask, runTask } from 'nitropack/runtime/internal/task';
export { defineNitroErrorHandler } from 'nitropack/runtime/internal/error/utils'; export { defineNitroErrorHandler } from 'nitropack/runtime/internal/error/utils';
export { buildAssetsURL as __buildAssetsURL, publicAssetsURL as __publicAssetsURL } from '/home/bennet/source/vat-api/node_modules/@nuxt/nitro-server/dist/runtime/utils/paths'; export { buildAssetsURL as __buildAssetsURL, publicAssetsURL as __publicAssetsURL } from '/home/bennet/source/vat-api/node_modules/@nuxt/nitro-server/dist/runtime/utils/paths';
export { defineAppConfig } from '/home/bennet/source/vat-api/node_modules/@nuxt/nitro-server/dist/runtime/utils/config'; export { defineAppConfig } from '/home/bennet/source/vat-api/node_modules/@nuxt/nitro-server/dist/runtime/utils/config';
export { getDb, closeDb } from '/home/bennet/source/vat-api/server/utils/mongodb';
export { getRedis, createRedisClient, closeRedis } from '/home/bennet/source/vat-api/server/utils/redis';
export { logRequest } from '/home/bennet/source/vat-api/server/utils/requestLogger';
export { getAllRates, getRateByCode } from '/home/bennet/source/vat-api/server/utils/vatRates'; export { getAllRates, getRateByCode } from '/home/bennet/source/vat-api/server/utils/vatRates';

View File

@@ -3,6 +3,15 @@ import type { Serialize, Simplify } from "nitropack/types";
declare module "nitropack/types" { declare module "nitropack/types" {
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T
interface InternalApi { interface InternalApi {
'/api/admin/login': {
'post': Simplify<Serialize<Awaited<ReturnType<typeof import('../../../../../server/api/admin/login.post').default>>>>
}
'/api/admin/requests': {
'get': Simplify<Serialize<Awaited<ReturnType<typeof import('../../../../../server/api/admin/requests.get').default>>>>
}
'/api/admin/stats': {
'get': Simplify<Serialize<Awaited<ReturnType<typeof import('../../../../../server/api/admin/stats.get').default>>>>
}
'/api/v1/rates/:code': { '/api/v1/rates/:code': {
'get': Simplify<Serialize<Awaited<ReturnType<typeof import('../../../../../server/api/v1/rates/[code].get').default>>>> 'get': Simplify<Serialize<Awaited<ReturnType<typeof import('../../../../../server/api/v1/rates/[code].get').default>>>>
} }

View File

@@ -12,6 +12,10 @@ import { RuntimeConfig as UserRuntimeConfig, PublicRuntimeConfig as UserPublicRu
vatstackApiKey: string, vatstackApiKey: string,
db: string,
adminPassword: string,
nitro: { nitro: {
envPrefix: string, envPrefix: string,
}, },

View File

@@ -1,25 +1,25 @@
{ {
"hash": "5d0f798f", "hash": "e7d1c9fa",
"configHash": "0abaf243", "configHash": "0abaf243",
"lockfileHash": "19c8bc86", "lockfileHash": "00c2deb1",
"browserHash": "a321d57f", "browserHash": "46083ee2",
"optimized": { "optimized": {
"errx": { "errx": {
"src": "../../../../errx/dist/index.js", "src": "../../../../errx/dist/index.js",
"file": "errx.js", "file": "errx.js",
"fileHash": "5dd32b92", "fileHash": "7d74e3df",
"needsInterop": false "needsInterop": false
}, },
"@vue/devtools-core": { "@vue/devtools-core": {
"src": "../../../../@vue/devtools-core/dist/index.js", "src": "../../../../@vue/devtools-core/dist/index.js",
"file": "@vue_devtools-core.js", "file": "@vue_devtools-core.js",
"fileHash": "307e8ff7", "fileHash": "1702c1a1",
"needsInterop": false "needsInterop": false
}, },
"@vue/devtools-kit": { "@vue/devtools-kit": {
"src": "../../../../@vue/devtools-kit/dist/index.js", "src": "../../../../@vue/devtools-kit/dist/index.js",
"file": "@vue_devtools-kit.js", "file": "@vue_devtools-kit.js",
"fileHash": "90e11ab0", "fileHash": "257d05dc",
"needsInterop": false "needsInterop": false
} }
}, },

139
node_modules/.package-lock.json generated vendored
View File

@@ -753,6 +753,14 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@mongodb-js/saslprep": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz",
"integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==",
"dependencies": {
"sparse-bitfield": "^3.0.3"
}
},
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
@@ -1883,6 +1891,19 @@
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/webidl-conversions": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
},
"node_modules/@types/whatwg-url": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
"integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
"dependencies": {
"@types/webidl-conversions": "*"
}
},
"node_modules/@unhead/vue": { "node_modules/@unhead/vue": {
"version": "2.1.4", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.4.tgz",
@@ -2915,6 +2936,14 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/bson": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
"integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/buffer": { "node_modules/buffer": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@@ -4883,7 +4912,6 @@
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.3.tgz", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.3.tgz",
"integrity": "sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==", "integrity": "sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@ioredis/commands": "1.5.0", "@ioredis/commands": "1.5.0",
"cluster-key-slot": "^1.1.0", "cluster-key-slot": "^1.1.0",
@@ -5713,6 +5741,11 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
},
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -5898,6 +5931,94 @@
"integrity": "sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==", "integrity": "sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/mongodb": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.1.0.tgz",
"integrity": "sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg==",
"dependencies": {
"@mongodb-js/saslprep": "^1.3.0",
"bson": "^7.1.1",
"mongodb-connection-string-url": "^7.0.0"
},
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@aws-sdk/credential-providers": "^3.806.0",
"@mongodb-js/zstd": "^7.0.0",
"gcp-metadata": "^7.0.1",
"kerberos": "^7.0.0",
"mongodb-client-encryption": ">=7.0.0 <7.1.0",
"snappy": "^7.3.2",
"socks": "^2.8.6"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-providers": {
"optional": true
},
"@mongodb-js/zstd": {
"optional": true
},
"gcp-metadata": {
"optional": true
},
"kerberos": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"snappy": {
"optional": true
},
"socks": {
"optional": true
}
}
},
"node_modules/mongodb-connection-string-url": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
"integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
"dependencies": {
"@types/whatwg-url": "^13.0.0",
"whatwg-url": "^14.1.0"
},
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"engines": {
"node": ">=12"
}
},
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/mrmime": { "node_modules/mrmime": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -7462,6 +7583,14 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"engines": {
"node": ">=6"
}
},
"node_modules/quansync": { "node_modules/quansync": {
"version": "0.2.11", "version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
@@ -8228,6 +8357,14 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
"dependencies": {
"memory-pager": "^1.0.2"
}
},
"node_modules/speakingurl": { "node_modules/speakingurl": {
"version": "14.0.1", "version": "14.0.1",
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",

View File

@@ -21,6 +21,9 @@ export default defineNuxtConfig({
runtimeConfig: { runtimeConfig: {
vatstackApiKey: '', vatstackApiKey: '',
db: '',
redisUrl: '',
adminPassword: '',
}, },
app: { app: {

141
package-lock.json generated
View File

@@ -8,6 +8,8 @@
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@oxc-parser/binding-linux-x64-gnu": "^0.112.0", "@oxc-parser/binding-linux-x64-gnu": "^0.112.0",
"ioredis": "^5.9.3",
"mongodb": "^7.1.0",
"nuxt": "^4.0.0", "nuxt": "^4.0.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
@@ -1169,6 +1171,14 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@mongodb-js/saslprep": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz",
"integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==",
"dependencies": {
"sparse-bitfield": "^3.0.3"
}
},
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
@@ -3682,6 +3692,19 @@
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/webidl-conversions": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
},
"node_modules/@types/whatwg-url": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
"integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
"dependencies": {
"@types/webidl-conversions": "*"
}
},
"node_modules/@unhead/vue": { "node_modules/@unhead/vue": {
"version": "2.1.4", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.4.tgz",
@@ -4714,6 +4737,14 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/bson": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
"integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/buffer": { "node_modules/buffer": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@@ -6696,7 +6727,6 @@
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.3.tgz", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.3.tgz",
"integrity": "sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==", "integrity": "sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@ioredis/commands": "1.5.0", "@ioredis/commands": "1.5.0",
"cluster-key-slot": "^1.1.0", "cluster-key-slot": "^1.1.0",
@@ -7526,6 +7556,11 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
},
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -7711,6 +7746,94 @@
"integrity": "sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==", "integrity": "sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/mongodb": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.1.0.tgz",
"integrity": "sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg==",
"dependencies": {
"@mongodb-js/saslprep": "^1.3.0",
"bson": "^7.1.1",
"mongodb-connection-string-url": "^7.0.0"
},
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@aws-sdk/credential-providers": "^3.806.0",
"@mongodb-js/zstd": "^7.0.0",
"gcp-metadata": "^7.0.1",
"kerberos": "^7.0.0",
"mongodb-client-encryption": ">=7.0.0 <7.1.0",
"snappy": "^7.3.2",
"socks": "^2.8.6"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-providers": {
"optional": true
},
"@mongodb-js/zstd": {
"optional": true
},
"gcp-metadata": {
"optional": true
},
"kerberos": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"snappy": {
"optional": true
},
"socks": {
"optional": true
}
}
},
"node_modules/mongodb-connection-string-url": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
"integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
"dependencies": {
"@types/whatwg-url": "^13.0.0",
"whatwg-url": "^14.1.0"
},
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"engines": {
"node": ">=12"
}
},
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/mrmime": { "node_modules/mrmime": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -9275,6 +9398,14 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"engines": {
"node": ">=6"
}
},
"node_modules/quansync": { "node_modules/quansync": {
"version": "0.2.11", "version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
@@ -10041,6 +10172,14 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
"dependencies": {
"memory-pager": "^1.0.2"
}
},
"node_modules/speakingurl": { "node_modules/speakingurl": {
"version": "14.0.1", "version": "14.0.1",
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",

View File

@@ -10,6 +10,8 @@
}, },
"dependencies": { "dependencies": {
"@oxc-parser/binding-linux-x64-gnu": "^0.112.0", "@oxc-parser/binding-linux-x64-gnu": "^0.112.0",
"ioredis": "^5.9.3",
"mongodb": "^7.1.0",
"nuxt": "^4.0.0", "nuxt": "^4.0.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"

View File

@@ -0,0 +1,28 @@
import { signToken } from "../../middleware/adminAuth";
export default defineEventHandler(async (event) => {
const body = await readBody<{ password?: string }>(event);
const adminPassword = useRuntimeConfig().adminPassword;
if (!adminPassword) {
throw createError({ statusCode: 500, statusMessage: "Admin password not configured" });
}
if (!body?.password || body.password !== adminPassword) {
throw createError({ statusCode: 401, statusMessage: "Invalid password" });
}
const payload = Date.now().toString(36);
const signature = signToken(payload);
const token = `${payload}.${signature}`;
setCookie(event, "admin-session", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 60 * 60 * 24, // 24 hours
path: "/",
});
return { ok: true };
});

View File

@@ -0,0 +1,31 @@
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const page = Math.max(1, parseInt(query.page as string) || 1);
const limit = Math.min(100, Math.max(1, parseInt(query.limit as string) || 50));
const ip = (query.ip as string) || undefined;
const db = await getDb();
const collection = db.collection("requests");
const filter: Record<string, unknown> = {};
if (ip) filter.ip = ip;
const [requests, total] = await Promise.all([
collection
.find(filter)
.sort({ timestamp: -1 })
.skip((page - 1) * limit)
.limit(limit)
.project({ _id: 0 })
.toArray(),
collection.countDocuments(filter),
]);
return {
requests,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
});

View File

@@ -0,0 +1,54 @@
export default defineEventHandler(async () => {
const db = await getDb();
const collection = db.collection("requests");
const now = new Date();
const last24h = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const last7d = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const last30d = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const [totalRequests24h, totalRequests7d, totalRequests30d, uniqueIPs, topIPs, requestsByHour] =
await Promise.all([
collection.countDocuments({ timestamp: { $gte: last24h.toISOString() } }),
collection.countDocuments({ timestamp: { $gte: last7d.toISOString() } }),
collection.countDocuments({ timestamp: { $gte: last30d.toISOString() } }),
collection
.distinct("ip", { timestamp: { $gte: last24h.toISOString() } })
.then((ips) => ips.length),
collection
.aggregate([
{ $match: { timestamp: { $gte: last24h.toISOString() } } },
{
$group: {
_id: "$ip",
count: { $sum: 1 },
lastSeen: { $max: "$timestamp" },
},
},
{ $sort: { count: -1 } },
{ $limit: 20 },
{ $project: { _id: 0, ip: "$_id", count: 1, lastSeen: 1 } },
])
.toArray(),
collection
.aggregate([
{ $match: { timestamp: { $gte: last24h.toISOString() } } },
{
$group: {
_id: { $substr: ["$timestamp", 11, 2] },
count: { $sum: 1 },
},
},
{ $sort: { _id: 1 } },
{ $project: { _id: 0, hour: "$_id", count: 1 } },
])
.toArray(),
]);
return {
totalRequests: { last24h: totalRequests24h, last7d: totalRequests7d, last30d: totalRequests30d },
uniqueIPs,
topIPs,
requestsByHour,
};
});

View File

@@ -0,0 +1,33 @@
import { createHmac, timingSafeEqual } from "node:crypto";
function getSecret(): string {
return useRuntimeConfig().adminPassword || "changeme";
}
export function signToken(payload: string): string {
return createHmac("sha256", getSecret()).update(payload).digest("hex");
}
export function verifyToken(payload: string, signature: string): boolean {
const expected = signToken(payload);
try {
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
} catch {
return false;
}
}
export default defineEventHandler((event) => {
const path = getRequestURL(event).pathname;
if (!path.startsWith("/api/admin/") || path === "/api/admin/login") return;
const cookie = getCookie(event, "admin-session");
if (!cookie) {
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
}
const [payload, signature] = cookie.split(".");
if (!payload || !signature || !verifyToken(payload, signature)) {
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
}
});

View File

@@ -0,0 +1,19 @@
export default defineEventHandler((event) => {
const path = getRequestURL(event).pathname;
if (!path.startsWith("/api/v1/rates")) return;
const ip = getRequestIP(event, { xForwardedFor: true }) ?? "127.0.0.1";
const method = event.method;
const userAgent = getRequestHeader(event, "user-agent") ?? "";
event.node.res.on("finish", () => {
logRequest({
ip,
path,
method,
statusCode: event.node.res.statusCode,
userAgent,
timestamp: new Date().toISOString(),
});
});
});

View File

@@ -0,0 +1,66 @@
import { createRedisClient, closeRedis } from "../utils/redis";
import { getDb, closeDb } from "../utils/mongodb";
export default defineNitroPlugin((nitro) => {
let running = true;
let workerRedis: ReturnType<typeof createRedisClient> | null = null;
const worker = async () => {
try {
workerRedis = createRedisClient();
const db = await getDb();
const collection = db.collection("requests");
while (running) {
const batch: Record<string, unknown>[] = [];
const deadline = Date.now() + 2000;
// Blocking pop — waits up to 5s for an item
const item = await workerRedis.brpop("request-logs", 5);
if (item) {
try {
batch.push(JSON.parse(item[1]));
} catch {}
}
// Drain up to 49 more items non-blocking
while (batch.length < 50) {
const next = await workerRedis.rpop("request-logs");
if (!next) break;
try {
batch.push(JSON.parse(next));
} catch {}
}
if (batch.length > 0) {
try {
await collection.insertMany(batch);
} catch (err) {
console.error("[logWorker] MongoDB insert failed:", err);
}
}
// If we haven't hit the deadline and batch was small, wait a bit
const remaining = deadline - Date.now();
if (remaining > 0 && batch.length < 10) {
await new Promise((r) => setTimeout(r, remaining));
}
}
} catch (err) {
console.error("[logWorker] Worker error:", err);
}
};
// Start worker in background
worker();
nitro.hooks.hook("close", async () => {
running = false;
if (workerRedis) {
await workerRedis.quit();
workerRedis = null;
}
await closeRedis();
await closeDb();
});
});

30
server/utils/mongodb.ts Normal file
View File

@@ -0,0 +1,30 @@
import { MongoClient, type Db } from "mongodb";
let client: MongoClient | null = null;
let db: Db | null = null;
export async function getDb(): Promise<Db> {
if (db) return db;
const uri = useRuntimeConfig().db;
if (!uri) throw new Error("NUXT_DB is not configured");
client = new MongoClient(uri);
await client.connect();
db = client.db("vat-api");
// Create indexes for the requests collection
const requests = db.collection("requests");
await requests.createIndex({ timestamp: -1 });
await requests.createIndex({ ip: 1, timestamp: -1 });
return db;
}
export async function closeDb(): Promise<void> {
if (client) {
await client.close();
client = null;
db = null;
}
}

24
server/utils/redis.ts Normal file
View File

@@ -0,0 +1,24 @@
import Redis from "ioredis";
let redis: Redis | null = null;
function getRedisUrl(): string {
return useRuntimeConfig().redisUrl || "redis://localhost:6379";
}
export function getRedis(): Redis {
if (redis) return redis;
redis = new Redis(getRedisUrl(), { maxRetriesPerRequest: null });
return redis;
}
export function createRedisClient(): Redis {
return new Redis(getRedisUrl(), { maxRetriesPerRequest: null });
}
export async function closeRedis(): Promise<void> {
if (redis) {
await redis.quit();
redis = null;
}
}

View File

@@ -0,0 +1,17 @@
export interface RequestLogEntry {
ip: string;
path: string;
method: string;
statusCode: number;
userAgent: string;
timestamp: string;
}
export function logRequest(data: RequestLogEntry): void {
try {
const redis = getRedis();
redis.lpush("request-logs", JSON.stringify(data)).catch(() => {});
} catch {
// Never let logging break API responses
}
}