From 7a6e400ce6b227c08f71669009db98738b7be04f Mon Sep 17 00:00:00 2001 From: shivendra0712 Date: Sun, 17 May 2026 21:06:17 +0530 Subject: [PATCH] design and build the Public REST API --- apps/backend/package.json | 4 + apps/backend/prisma/schema.prisma | 14 + apps/backend/src/__tests__/apiKey.test.ts | 194 +++++++++++++ apps/backend/src/app.ts | 35 +++ apps/backend/src/plugins/apiKey.ts | 59 ++++ apps/backend/src/routes/v1.ts | 330 ++++++++++++++++++++++ apps/web/src/routes/settings/+page.svelte | 78 +++++ pnpm-lock.yaml | 91 ++++++ 8 files changed, 805 insertions(+) create mode 100644 apps/backend/src/__tests__/apiKey.test.ts create mode 100644 apps/backend/src/plugins/apiKey.ts create mode 100644 apps/backend/src/routes/v1.ts create mode 100644 apps/web/src/routes/settings/+page.svelte diff --git a/apps/backend/package.json b/apps/backend/package.json index b8d1141..767e76e 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -24,6 +24,9 @@ "@fastify/multipart": "^9.0.0", "@fastify/static": "^8.0.0", "@prisma/client": "^6.0.0", + "@fastify/swagger": "^9.7.0", + "@fastify/swagger-ui": "^5.2.6", + "bcryptjs": "^2.4.3", "dotenv": "^16.4.0", "fastify": "^5.0.0", "fastify-plugin": "^5.0.0", @@ -33,6 +36,7 @@ }, "devDependencies": { "@types/node": "^22.0.0", + "@types/bcryptjs": "^2.4.2", "@types/qrcode": "^1.5.0", "pino-pretty": "^13.1.3", "prisma": "^6.0.0", diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 13dec57..206fd59 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -94,6 +94,20 @@ model OAuthToken { @@map("oauth_tokens") } +model ApiKey { + id String @id @default(uuid()) + userId String @map("user_id") + keyHash String @map("key_hash") + label String? + lastUsed DateTime? @map("last_used") + createdAt DateTime @default(now()) @map("created_at") + revokedAt DateTime? @map("revoked_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("api_keys") +} + model CardView { id String @id @default(uuid()) cardId String? @map("card_id") // null = default profile view diff --git a/apps/backend/src/__tests__/apiKey.test.ts b/apps/backend/src/__tests__/apiKey.test.ts new file mode 100644 index 0000000..8bb9afc --- /dev/null +++ b/apps/backend/src/__tests__/apiKey.test.ts @@ -0,0 +1,194 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import Fastify from 'fastify'; +import swagger from '@fastify/swagger'; +import { apiKeyPlugin } from '../plugins/apiKey.js'; +import { v1Routes } from '../routes/v1.js'; +import * as bcrypt from 'bcryptjs'; + +const mockPrisma = { + apiKey: { + findUnique: vi.fn(), + update: vi.fn(), + create: vi.fn(), + }, + user: { + findUnique: vi.fn(), + }, + platformLink: { + aggregate: vi.fn(), + create: vi.fn(), + findFirst: vi.fn(), + delete: vi.fn(), + }, +}; + +const mockRedis = { + incr: vi.fn(), + expire: vi.fn(), +}; + +async function buildApp() { + const app = Fastify(); + app.decorate('prisma', mockPrisma); + app.decorate('redis', mockRedis); + await app.register(swagger, { + openapi: { + openapi: '3.1.0', + info: { title: 'DevCard Public API', version: '1.0.0' }, + servers: [{ url: 'http://localhost:3000/api/v1' }], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'API Key', + }, + }, + }, + }, + exposeRoute: false, + }); + app.register(apiKeyPlugin); + app.decorate('authenticate', async (request: any) => { + request.user = { id: 'user-abc' }; + }); + app.register(v1Routes, { prefix: '/api/v1' }); + await app.ready(); + return app; +} + +describe('Public v1 API', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('creates an API key and returns the raw secret once', async () => { + mockPrisma.apiKey.create.mockResolvedValue({ + id: 'key-abc', + userId: 'user-abc', + label: 'test key', + createdAt: new Date('2026-01-01T00:00:00.000Z'), + lastUsed: null, + revokedAt: null, + }); + + const app = await buildApp(); + const res = await app.inject({ method: 'POST', url: '/api/v1/keys', payload: { label: 'test key' } }); + expect(res.statusCode).toBe(201); + const body = res.json(); + expect(body.id).toBe('key-abc'); + expect(body.label).toBe('test key'); + expect(typeof body.key).toBe('string'); + expect(body.key.startsWith('key-abc.')).toBe(true); + expect(mockPrisma.apiKey.create).toHaveBeenCalled(); + }); + + it('allows a valid API key to fetch authenticated profile data', async () => { + const secret = 'my-secret-token'; + const hashValue = await bcrypt.hash(secret, 10); + const apiKeyRecord = { + id: 'auth-key', + userId: 'user-abc', + keyHash: hashValue, + revokedAt: null, + }; + + mockPrisma.apiKey.findUnique.mockResolvedValue(apiKeyRecord); + mockPrisma.apiKey.update.mockResolvedValue({ ...apiKeyRecord, lastUsed: new Date() }); + mockRedis.incr.mockResolvedValue(1); + mockRedis.expire.mockResolvedValue(1); + mockPrisma.user.findUnique.mockResolvedValue({ + id: 'user-abc', + email: 'api@example.com', + username: 'apiprofile', + displayName: 'API User', + bio: 'API bio', + pronouns: 'they/them', + role: 'developer', + company: 'DevCard', + avatarUrl: null, + accentColor: '#ff0000', + platformLinks: [], + cards: [], + }); + + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/v1/profiles/me', + headers: { authorization: `Bearer auth-key.${secret}` }, + }); + + expect(res.statusCode).toBe(200); + expect(res.json()).toEqual(expect.objectContaining({ username: 'apiprofile', email: 'api@example.com' })); + expect(mockPrisma.apiKey.update).toHaveBeenCalled(); + }); + + it('revokes an API key and rejects it on next request', async () => { + const secret = 'revoked-secret'; + const hashValue = await bcrypt.hash(secret, 10); + const currentKey = { + id: 'revoked-key', + userId: 'user-abc', + keyHash: hashValue, + revokedAt: null, + }; + + mockPrisma.apiKey.findUnique.mockImplementation(async ({ where }: any) => { + if (where?.id === 'revoked-key') { + return currentKey; + } + return null; + }); + mockPrisma.apiKey.update.mockImplementation(async ({ where }: any) => ({ ...currentKey, ...where })); + + const app = await buildApp(); + const deleteRes = await app.inject({ method: 'DELETE', url: '/api/v1/keys/revoked-key' }); + + expect(deleteRes.statusCode).toBe(204); + expect(mockPrisma.apiKey.update).toHaveBeenCalledWith({ where: { id: 'revoked-key' }, data: { revokedAt: expect.any(Date) } }); + + mockPrisma.apiKey.findUnique.mockResolvedValue({ ...currentKey, revokedAt: new Date() }); + const fetchRes = await app.inject({ + method: 'GET', + url: '/api/v1/profiles/me', + headers: { authorization: `Bearer revoked-key.${secret}` }, + }); + + expect(fetchRes.statusCode).toBe(401); + expect(fetchRes.json().error).toBe('Unauthorized'); + }); + + it('returns public profile data without an API key', async () => { + mockPrisma.user.findUnique.mockResolvedValue({ + id: 'user-xyz', + username: 'publicuser', + displayName: 'Public User', + bio: 'Public bio', + pronouns: null, + role: null, + company: null, + avatarUrl: null, + accentColor: '#000000', + platformLinks: [], + cards: [], + }); + + const app = await buildApp(); + const res = await app.inject({ method: 'GET', url: '/api/v1/profiles/publicuser' }); + + expect(res.statusCode).toBe(200); + expect(res.json().username).toBe('publicuser'); + }); + + it('serves OpenAPI JSON at /api/v1/openapi.json', async () => { + const app = await buildApp(); + const res = await app.inject({ method: 'GET', url: '/api/v1/openapi.json' }); + + expect(res.statusCode).toBe(200); + expect(res.headers['content-type']).toContain('application/json'); + const body = res.json(); + expect(body.openapi).toBe('3.1.0'); + expect(body.paths['/profiles/me']).toBeDefined(); + }); +}); diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index 8e8cf38..1c25920 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -7,9 +7,12 @@ import multipart from '@fastify/multipart'; import fastifyStatic from '@fastify/static'; import path from 'path'; import { fileURLToPath } from 'url'; +import swagger from '@fastify/swagger'; +import swaggerUi from '@fastify/swagger-ui'; import { prismaPlugin } from './plugins/prisma.js'; import { redisPlugin } from './plugins/redis.js'; +import { apiKeyPlugin } from './plugins/apiKey.js'; import { authRoutes } from './routes/auth.js'; import { profileRoutes } from './routes/profiles.js'; import { cardRoutes } from './routes/cards.js'; @@ -17,6 +20,7 @@ import { publicRoutes } from './routes/public.js'; import { followRoutes } from './routes/follow.js'; import { connectRoutes } from './routes/connect.js'; import { analyticsRoutes } from './routes/analytics.js'; +import { v1Routes } from './routes/v1.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -71,6 +75,36 @@ export async function buildApp() { // ─── Database & Cache Plugins ─── await app.register(prismaPlugin); await app.register(redisPlugin); + await app.register(apiKeyPlugin); + + await app.register(swagger, { + openapi: { + openapi: '3.1.0', + info: { + title: 'DevCard Public API', + description: 'Versioned public REST API for DevCard.', + version: '1.0.0', + }, + servers: [{ url: process.env.PUBLIC_API_URL || 'http://localhost:3000/api/v1' }], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'API Key', + }, + }, + }, + }, + }); + + await app.register(swaggerUi, { + routePrefix: '/api/v1/docs', + staticCSP: true, + swagger: { + url: '/api/v1/openapi.json', + }, + }); // ─── Auth Decorator ─── app.decorate('authenticate', async function (request: any, reply: any) { @@ -89,6 +123,7 @@ export async function buildApp() { await app.register(followRoutes, { prefix: '/api/follow' }); await app.register(connectRoutes, { prefix: '/api/connect' }); await app.register(analyticsRoutes, { prefix: '/api/analytics' }); + await app.register(v1Routes, { prefix: '/api/v1' }); // ─── Health Check ─── app.get('/health', async () => ({ diff --git a/apps/backend/src/plugins/apiKey.ts b/apps/backend/src/plugins/apiKey.ts new file mode 100644 index 0000000..99cc83f --- /dev/null +++ b/apps/backend/src/plugins/apiKey.ts @@ -0,0 +1,59 @@ +import fp from 'fastify-plugin'; +import * as bcrypt from 'bcryptjs'; +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; + +declare module 'fastify' { + interface FastifyInstance { + verifyApiKey: (request: FastifyRequest, reply: FastifyReply) => Promise; + } + + interface FastifyRequest { + apiKey?: { + id: string; + userId: string; + }; + } +} + +export const apiKeyPlugin = fp(async (app: FastifyInstance) => { + app.decorateRequest('apiKey', undefined as any); + + app.decorate('verifyApiKey', async function (request: FastifyRequest, reply: FastifyReply) { + const header = request.headers.authorization; + if (typeof header !== 'string' || !header.startsWith('Bearer ')) { + return reply.status(401).send({ error: 'Unauthorized' }); + } + + const token = header.slice(7).trim(); + const [id, secret] = token.split('.', 2); + if (!id || !secret) { + return reply.status(401).send({ error: 'Unauthorized' }); + } + + const apiKey = await app.prisma.apiKey.findUnique({ where: { id } }); + if (!apiKey || apiKey.revokedAt) { + return reply.status(401).send({ error: 'Unauthorized' }); + } + + const isMatch = await bcrypt.compare(secret, apiKey.keyHash); + if (!isMatch) { + return reply.status(401).send({ error: 'Unauthorized' }); + } + + await app.prisma.apiKey.update({ where: { id }, data: { lastUsed: new Date() } }); + request.apiKey = { id: apiKey.id, userId: apiKey.userId }; + + // Separate API key rate limiting for versioned public API traffic. + if (app.redis) { + const windowSeconds = 60; + const rateKey = `rate:api:${apiKey.id}:${Math.floor(Date.now() / 1000 / windowSeconds)}`; + const count = await app.redis.incr(rateKey); + if (count === 1) { + await app.redis.expire(rateKey, windowSeconds); + } + if (count > 300) { + return reply.status(429).send({ error: 'Too Many Requests' }); + } + } + }); +}); diff --git a/apps/backend/src/routes/v1.ts b/apps/backend/src/routes/v1.ts new file mode 100644 index 0000000..225988f --- /dev/null +++ b/apps/backend/src/routes/v1.ts @@ -0,0 +1,330 @@ +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import crypto from 'node:crypto'; +import * as bcrypt from 'bcryptjs'; +import { z } from 'zod'; +import { getProfileUrl } from '@devcard/shared'; +import { createLinkSchema } from '../utils/validators.js'; + +const createApiKeySchema = z.object({ + label: z.string().max(100).optional(), +}); + +const platformLinkSchema = { + type: 'object', + properties: { + id: { type: 'string' }, + platform: { type: 'string' }, + username: { type: 'string' }, + url: { type: 'string' }, + displayOrder: { type: 'number' }, + }, + required: ['id', 'platform', 'username', 'url', 'displayOrder'], +}; + +const fullProfileSchema = { + type: 'object', + properties: { + id: { type: 'string' }, + email: { type: 'string' }, + username: { type: 'string' }, + displayName: { type: ['string', 'null'] }, + bio: { type: ['string', 'null'] }, + pronouns: { type: ['string', 'null'] }, + role: { type: ['string', 'null'] }, + company: { type: ['string', 'null'] }, + avatarUrl: { type: ['string', 'null'] }, + accentColor: { type: 'string' }, + platformLinks: { type: 'array', items: platformLinkSchema }, + defaultCardId: { type: ['string', 'null'] }, + }, + required: ['id', 'username', 'accentColor', 'platformLinks', 'defaultCardId'], +}; + +const publicProfileSchema = { + type: 'object', + properties: { + id: { type: 'string' }, + username: { type: 'string' }, + displayName: { type: ['string', 'null'] }, + bio: { type: ['string', 'null'] }, + pronouns: { type: ['string', 'null'] }, + role: { type: ['string', 'null'] }, + company: { type: ['string', 'null'] }, + avatarUrl: { type: ['string', 'null'] }, + accentColor: { type: 'string' }, + platformLinks: { type: 'array', items: platformLinkSchema }, + defaultCardId: { type: ['string', 'null'] }, + }, + required: ['id', 'username', 'accentColor', 'platformLinks', 'defaultCardId'], +}; + +const createApiKeySchemaDefinition = { + type: 'object', + properties: { + label: { type: 'string' }, + }, + additionalProperties: false, +}; + +const apiKeyResponseSchema = { + type: 'object', + properties: { + id: { type: 'string' }, + label: { type: ['string', 'null'] }, + createdAt: { type: 'string', format: 'date-time' }, + lastUsed: { type: ['string', 'null'], format: 'date-time' }, + revokedAt: { type: ['string', 'null'], format: 'date-time' }, + key: { type: 'string' }, + }, + required: ['id', 'createdAt', 'key'], +}; + +export async function v1Routes(app: FastifyInstance) { + const apiKeyRoutes = async (router: FastifyInstance) => { + router.addHook('preHandler', app.authenticate); + + router.post( + '/', + { + schema: { + tags: ['Public API'], + body: createApiKeySchemaDefinition, + response: { 201: apiKeyResponseSchema }, + }, + }, + async (request: FastifyRequest, reply: FastifyReply) => { + const userId = (request.user as any).id; + const parsed = createApiKeySchema.safeParse(request.body); + if (!parsed.success) { + return reply.status(400).send({ error: 'Validation failed', details: parsed.error.flatten() }); + } + + const secret = crypto.randomBytes(32).toString('hex'); + const keyHash = await bcrypt.hash(secret, 10); + const label = typeof parsed.data.label === 'string' ? parsed.data.label : null; + const apiKey = await app.prisma.apiKey.create({ + data: { + userId, + keyHash, + label, + }, + }); + + return reply.status(201).send({ + id: apiKey.id, + label: apiKey.label, + createdAt: apiKey.createdAt, + lastUsed: apiKey.lastUsed, + revokedAt: apiKey.revokedAt, + key: `${apiKey.id}.${secret}`, + }); + } + ); + + router.delete( + '/:id', + { + schema: { + tags: ['Public API'], + params: { + type: 'object', + properties: { id: { type: 'string' } }, + required: ['id'], + }, + response: { 204: { type: 'null' } }, + }, + }, + async (request, reply) => { + const userId = (request.user as any).id; + const { id } = request.params; + + const current = await app.prisma.apiKey.findUnique({ where: { id } }); + if (!current || current.userId !== userId) { + return reply.status(404).send({ error: 'API key not found' }); + } + + await app.prisma.apiKey.update({ where: { id }, data: { revokedAt: new Date() } }); + return reply.status(204).send(); + } + ); + }; + + const profileRoutes = async (router: FastifyInstance) => { + router.get( + '/me', + { + preHandler: app.verifyApiKey, + schema: { + tags: ['Public API'], + security: [{ bearerAuth: [] }], + response: { 200: fullProfileSchema }, + }, + }, + async (request: FastifyRequest, reply: FastifyReply) => { + const userId = request.apiKey?.userId; + if (!userId) { + return reply.status(401).send({ error: 'Unauthorized' }); + } + + const user = await app.prisma.user.findUnique({ + where: { id: userId }, + include: { + platformLinks: { orderBy: { displayOrder: 'asc' } }, + cards: { where: { isDefault: true }, select: { id: true }, take: 1 }, + }, + }); + + if (!user) { + return reply.status(404).send({ error: 'User not found' }); + } + + return { + id: user.id, + email: user.email, + username: user.username, + displayName: user.displayName, + bio: user.bio, + pronouns: user.pronouns, + role: user.role, + company: user.company, + avatarUrl: user.avatarUrl, + accentColor: user.accentColor, + platformLinks: user.platformLinks, + defaultCardId: user.cards[0]?.id || null, + }; + } + ); + + router.put( + '/me/links', + { + preHandler: app.verifyApiKey, + schema: { + tags: ['Public API'], + security: [{ bearerAuth: [] }], + body: { + type: 'object', + properties: { + platform: { type: 'string' }, + username: { type: 'string' }, + url: { type: 'string' }, + }, + required: ['platform', 'username'], + }, + response: { 201: platformLinkSchema }, + }, + }, + async (request: FastifyRequest, reply: FastifyReply) => { + const userId = request.apiKey?.userId; + if (!userId) { + return reply.status(401).send({ error: 'Unauthorized' }); + } + + const parsed = createLinkSchema.safeParse(request.body); + if (!parsed.success) { + return reply.status(400).send({ error: 'Validation failed', details: parsed.error.flatten() }); + } + + const url = parsed.data.url || getProfileUrl(parsed.data.platform, parsed.data.username); + const maxOrder = await app.prisma.platformLink.aggregate({ + where: { userId }, + _max: { displayOrder: true }, + }); + + const link = await app.prisma.platformLink.create({ + data: { + userId, + platform: parsed.data.platform, + username: parsed.data.username, + url, + displayOrder: (maxOrder._max.displayOrder ?? -1) + 1, + }, + }); + + return reply.status(201).send(link); + } + ); + + router.delete( + '/me/links/:id', + { + preHandler: app.verifyApiKey, + schema: { + tags: ['Public API'], + security: [{ bearerAuth: [] }], + params: { + type: 'object', + properties: { id: { type: 'string' } }, + required: ['id'], + }, + response: { 204: { type: 'null' } }, + }, + }, + async (request, reply) => { + const userId = request.apiKey?.userId; + if (!userId) { + return reply.status(401).send({ error: 'Unauthorized' }); + } + + const { id } = request.params; + const existing = await app.prisma.platformLink.findFirst({ where: { id, userId } }); + if (!existing) { + return reply.status(404).send({ error: 'Link not found' }); + } + + await app.prisma.platformLink.delete({ where: { id } }); + return reply.status(204).send(); + } + ); + + router.get( + '/:username', + { + schema: { + tags: ['Public API'], + params: { + type: 'object', + properties: { username: { type: 'string' } }, + required: ['username'], + }, + response: { 200: publicProfileSchema }, + }, + }, + async (request, reply) => { + const { username } = request.params; + const user = await app.prisma.user.findUnique({ + where: { username }, + include: { + platformLinks: { orderBy: { displayOrder: 'asc' } }, + cards: { where: { isDefault: true }, select: { id: true }, take: 1 }, + }, + }); + + if (!user) { + return reply.status(404).send({ error: 'Profile not found' }); + } + + return { + id: user.id, + username: user.username, + displayName: user.displayName, + bio: user.bio, + pronouns: user.pronouns, + role: user.role, + company: user.company, + avatarUrl: user.avatarUrl, + accentColor: user.accentColor, + platformLinks: user.platformLinks, + defaultCardId: user.cards[0]?.id || null, + }; + } + ); + }; + + await app.register(apiKeyRoutes, { prefix: '/keys' }); + await app.register(profileRoutes, { prefix: '/profiles' }); + + app.get('/openapi.json', async (request: FastifyRequest, reply: FastifyReply) => { + return reply.send(app.swagger()); + }); +} diff --git a/apps/web/src/routes/settings/+page.svelte b/apps/web/src/routes/settings/+page.svelte new file mode 100644 index 0000000..bf3d2ab --- /dev/null +++ b/apps/web/src/routes/settings/+page.svelte @@ -0,0 +1,78 @@ + + + + DevCard Settings + + +
+

Settings

+
+

API Key Management

+

+ Manage your DevCard API keys here. You can create new keys for automated workflows and revoke + keys that should no longer have access. +

+
+

This is a stub UI for API key management. The public API docs are available at:

+ /api/v1/docs +
+
+ {#each apiKeys as apiKey} +
+

{apiKey.name}

+

{apiKey.description}

+
+ {/each} +
+
+
+ + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6818604..a0383a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,18 @@ importers: '@fastify/static': specifier: ^8.0.0 version: 8.3.0 + '@fastify/swagger': + specifier: ^9.7.0 + version: 9.7.0 + '@fastify/swagger-ui': + specifier: ^5.2.6 + version: 5.2.6 '@prisma/client': specifier: ^6.0.0 version: 6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3) + bcryptjs: + specifier: ^2.4.3 + version: 2.4.3 dotenv: specifier: ^16.4.0 version: 16.6.1 @@ -57,6 +66,9 @@ importers: specifier: ^3.23.0 version: 3.25.76 devDependencies: + '@types/bcryptjs': + specifier: ^2.4.2 + version: 2.4.6 '@types/node': specifier: ^22.0.0 version: 22.19.15 @@ -1267,6 +1279,15 @@ packages: '@fastify/static@8.3.0': resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==} + '@fastify/static@9.1.3': + resolution: {integrity: sha512-aXrYtsiryLhRxRNaxNqsn7FUISeb7rB9q4eHUPIot5aeQBLNahnz1m6thzm7JWC1poSGXS9XrX8DvuMivp2hkQ==} + + '@fastify/swagger-ui@5.2.6': + resolution: {integrity: sha512-OMnms0O5s9wb6wis/K5nlrAMLsgUbr1GA8uphM41IasWe3AFdgxz6r/3bA9HTxlDNUYc2FGGKeqMp3ntxmSiNA==} + + '@fastify/swagger@9.7.0': + resolution: {integrity: sha512-Vp1SC1GC2Hrkd3faFILv86BzUNyFz5N4/xdExqtCgkGASOzn/x+eMe4qXIGq7cdT6wif/P/oa6r1Ruqx19paZA==} + '@gorhom/bottom-sheet@5.2.14': resolution: {integrity: sha512-uLQFlDjp9z+jrOFcMSEldPqL5JdaXL3vXOh+juhwoNvXgTsEorJLjHTugXu+YccAG/0KJnShzKCrb71MHBsvJg==} peerDependencies: @@ -1873,6 +1894,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/bcryptjs@2.4.6': + resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -2241,6 +2265,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bcryptjs@2.4.3: + resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -2474,6 +2501,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -3152,6 +3183,10 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -3668,6 +3703,10 @@ packages: json-schema-ref-resolver@3.0.0: resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-resolver@3.0.0: + resolution: {integrity: sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==} + engines: {node: '>=20'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -4092,6 +4131,9 @@ packages: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -6382,6 +6424,33 @@ snapshots: fastq: 1.20.1 glob: 11.1.0 + '@fastify/static@9.1.3': + dependencies: + '@fastify/accept-negotiator': 2.0.1 + '@fastify/send': 4.1.0 + content-disposition: 1.1.0 + fastify-plugin: 5.1.0 + fastq: 1.20.1 + glob: 13.0.6 + + '@fastify/swagger-ui@5.2.6': + dependencies: + '@fastify/static': 9.1.3 + fastify-plugin: 5.1.0 + openapi-types: 12.1.3 + rfdc: 1.4.1 + yaml: 2.8.2 + + '@fastify/swagger@9.7.0': + dependencies: + fastify-plugin: 5.1.0 + json-schema-resolver: 3.0.0 + openapi-types: 12.1.3 + rfdc: 1.4.1 + yaml: 2.8.2 + transitivePeerDependencies: + - supports-color + '@gorhom/bottom-sheet@5.2.14(@types/react-native@0.70.19)(@types/react@19.2.14)(react-native-gesture-handler@2.31.2(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-reanimated@3.19.5(@babel/core@7.29.0)(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)': dependencies: '@gorhom/portal': 1.0.14(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.1.0(typescript@5.9.3))(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) @@ -7213,6 +7282,8 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/bcryptjs@2.4.6': {} + '@types/cookie@0.6.0': {} '@types/estree@1.0.8': {} @@ -7683,6 +7754,8 @@ snapshots: baseline-browser-mapping@2.10.0: {} + bcryptjs@2.4.3: {} + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -7951,6 +8024,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.1.0: {} + content-type@1.0.5: {} convert-source-map@2.0.0: {} @@ -8799,6 +8874,12 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 2.0.2 + glob@13.0.6: + dependencies: + minimatch: 10.2.4 + minipass: 7.1.3 + path-scurry: 2.0.2 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -9498,6 +9579,14 @@ snapshots: dependencies: dequal: 2.0.3 + json-schema-resolver@3.0.0: + dependencies: + debug: 4.4.3 + fast-uri: 3.1.0 + rfdc: 1.4.1 + transitivePeerDependencies: + - supports-color + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -9984,6 +10073,8 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openapi-types@12.1.3: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4