From ef55d9bb24430610b60c1b77821307cd72c08c55 Mon Sep 17 00:00:00 2001 From: zwlucas Date: Sat, 25 Jul 2026 18:23:29 -0300 Subject: feat: upload of the project Signed-off-by: zwlucas --- .gitattributes | 11 + .gitignore | 52 + GEMINI.md | 35 + GIT_SETUP.sh | 97 + README.md | 76 + api/.env.example | 23 + api/drizzle.config.ts | 32 + api/package.json | 40 + api/src/db/connection.ts | 37 + api/src/db/migrate.ts | 56 + api/src/db/migrations/0000_huge_luke_cage.sql | 61 + .../db/migrations/0001_ambitious_emma_frost.sql | 6 + api/src/db/migrations/meta/0000_snapshot.json | 446 +++ api/src/db/migrations/meta/0001_snapshot.json | 486 +++ api/src/db/migrations/meta/_journal.json | 20 + api/src/db/schema.ts | 240 ++ api/src/db/seed.ts | 60 + api/src/modules/auth/auth.controller.ts | 114 + api/src/modules/auth/auth.repository.ts | 68 + api/src/modules/auth/auth.routes.ts | 55 + api/src/modules/auth/auth.schema.ts | 66 + .../modules/categories/categories.controller.ts | 127 + .../modules/categories/categories.repository.ts | 80 + api/src/modules/categories/categories.routes.ts | 92 + api/src/modules/categories/categories.schema.ts | 76 + .../interactions/interactions.controller.ts | 190 + .../interactions/interactions.repository.ts | 135 + .../modules/interactions/interactions.routes.ts | 86 + .../modules/interactions/interactions.schema.ts | 101 + api/src/modules/settings/settings.controller.ts | 24 + api/src/modules/settings/settings.repository.ts | 26 + api/src/modules/settings/settings.routes.ts | 30 + api/src/modules/settings/settings.schema.ts | 7 + api/src/modules/stats/stats.controller.ts | 15 + api/src/modules/stats/stats.repository.ts | 41 + api/src/modules/stats/stats.routes.ts | 22 + api/src/modules/stats/stats.schema.ts | 17 + api/src/modules/tickets/tickets.controller.ts | 112 + api/src/modules/tickets/tickets.repository.ts | 145 + api/src/modules/tickets/tickets.routes.ts | 75 + api/src/modules/tickets/tickets.schema.ts | 89 + api/src/modules/users/users.controller.ts | 129 + api/src/modules/users/users.routes.ts | 56 + api/src/modules/users/users.schema.ts | 25 + api/src/plugins/auth.plugin.ts | 93 + api/src/plugins/db.plugin.ts | 31 + api/src/plugins/events.plugin.ts | 34 + api/src/plugins/events.routes.ts | 58 + api/src/script.js | 5 + api/src/server.ts | 115 + api/tsconfig.json | 37 + package.json | 21 + pnpm-lock.yaml | 4102 ++++++++++++++++++++ pnpm-workspace.yaml | 6 + web/index.html | 27 + web/package.json | 37 + web/src/App.tsx | 39 + web/src/components/auth/ProtectedRoute.tsx | 20 + web/src/components/layout/AppLayout.module.css | 194 + web/src/components/layout/AppLayout.tsx | 199 + web/src/components/tickets/PriorityBadge.tsx | 25 + web/src/components/tickets/StatusBadge.tsx | 26 + web/src/components/tickets/TicketCard.module.css | 94 + web/src/components/tickets/TicketCard.tsx | 66 + web/src/components/ui/PageLoader.module.css | 26 + web/src/components/ui/PageLoader.tsx | 28 + web/src/context/AuthContext.tsx | 79 + web/src/context/Toast.module.css | 75 + web/src/context/ToastContext.tsx | 71 + web/src/hooks/useTicketEvents.ts | 91 + web/src/lib/api.ts | 297 ++ web/src/main.tsx | 16 + web/src/pages/DashboardPage.module.css | 160 + web/src/pages/DashboardPage.tsx | 224 ++ web/src/pages/LoginPage.module.css | 125 + web/src/pages/LoginPage.tsx | 124 + web/src/pages/NewTicketPage.module.css | 88 + web/src/pages/NewTicketPage.tsx | 203 + web/src/pages/SettingsPage.module.css | 111 + web/src/pages/SettingsPage.tsx | 63 + web/src/pages/TicketDetailPage.module.css | 263 ++ web/src/pages/TicketDetailPage.tsx | 366 ++ web/src/pages/UsersPage.module.css | 121 + web/src/pages/UsersPage.tsx | 290 ++ web/src/router.tsx | 70 + web/src/styles/index.css | 274 ++ web/tsconfig.app.json | 32 + web/tsconfig.json | 7 + web/tsconfig.node.json | 12 + web/vite.config.ts | 33 + 90 files changed, 12259 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 GEMINI.md create mode 100644 GIT_SETUP.sh create mode 100644 README.md create mode 100644 api/.env.example create mode 100644 api/drizzle.config.ts create mode 100644 api/package.json create mode 100644 api/src/db/connection.ts create mode 100644 api/src/db/migrate.ts create mode 100644 api/src/db/migrations/0000_huge_luke_cage.sql create mode 100644 api/src/db/migrations/0001_ambitious_emma_frost.sql create mode 100644 api/src/db/migrations/meta/0000_snapshot.json create mode 100644 api/src/db/migrations/meta/0001_snapshot.json create mode 100644 api/src/db/migrations/meta/_journal.json create mode 100644 api/src/db/schema.ts create mode 100644 api/src/db/seed.ts create mode 100644 api/src/modules/auth/auth.controller.ts create mode 100644 api/src/modules/auth/auth.repository.ts create mode 100644 api/src/modules/auth/auth.routes.ts create mode 100644 api/src/modules/auth/auth.schema.ts create mode 100644 api/src/modules/categories/categories.controller.ts create mode 100644 api/src/modules/categories/categories.repository.ts create mode 100644 api/src/modules/categories/categories.routes.ts create mode 100644 api/src/modules/categories/categories.schema.ts create mode 100644 api/src/modules/interactions/interactions.controller.ts create mode 100644 api/src/modules/interactions/interactions.repository.ts create mode 100644 api/src/modules/interactions/interactions.routes.ts create mode 100644 api/src/modules/interactions/interactions.schema.ts create mode 100644 api/src/modules/settings/settings.controller.ts create mode 100644 api/src/modules/settings/settings.repository.ts create mode 100644 api/src/modules/settings/settings.routes.ts create mode 100644 api/src/modules/settings/settings.schema.ts create mode 100644 api/src/modules/stats/stats.controller.ts create mode 100644 api/src/modules/stats/stats.repository.ts create mode 100644 api/src/modules/stats/stats.routes.ts create mode 100644 api/src/modules/stats/stats.schema.ts create mode 100644 api/src/modules/tickets/tickets.controller.ts create mode 100644 api/src/modules/tickets/tickets.repository.ts create mode 100644 api/src/modules/tickets/tickets.routes.ts create mode 100644 api/src/modules/tickets/tickets.schema.ts create mode 100644 api/src/modules/users/users.controller.ts create mode 100644 api/src/modules/users/users.routes.ts create mode 100644 api/src/modules/users/users.schema.ts create mode 100644 api/src/plugins/auth.plugin.ts create mode 100644 api/src/plugins/db.plugin.ts create mode 100644 api/src/plugins/events.plugin.ts create mode 100644 api/src/plugins/events.routes.ts create mode 100644 api/src/script.js create mode 100644 api/src/server.ts create mode 100644 api/tsconfig.json create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 web/index.html create mode 100644 web/package.json create mode 100644 web/src/App.tsx create mode 100644 web/src/components/auth/ProtectedRoute.tsx create mode 100644 web/src/components/layout/AppLayout.module.css create mode 100644 web/src/components/layout/AppLayout.tsx create mode 100644 web/src/components/tickets/PriorityBadge.tsx create mode 100644 web/src/components/tickets/StatusBadge.tsx create mode 100644 web/src/components/tickets/TicketCard.module.css create mode 100644 web/src/components/tickets/TicketCard.tsx create mode 100644 web/src/components/ui/PageLoader.module.css create mode 100644 web/src/components/ui/PageLoader.tsx create mode 100644 web/src/context/AuthContext.tsx create mode 100644 web/src/context/Toast.module.css create mode 100644 web/src/context/ToastContext.tsx create mode 100644 web/src/hooks/useTicketEvents.ts create mode 100644 web/src/lib/api.ts create mode 100644 web/src/main.tsx create mode 100644 web/src/pages/DashboardPage.module.css create mode 100644 web/src/pages/DashboardPage.tsx create mode 100644 web/src/pages/LoginPage.module.css create mode 100644 web/src/pages/LoginPage.tsx create mode 100644 web/src/pages/NewTicketPage.module.css create mode 100644 web/src/pages/NewTicketPage.tsx create mode 100644 web/src/pages/SettingsPage.module.css create mode 100644 web/src/pages/SettingsPage.tsx create mode 100644 web/src/pages/TicketDetailPage.module.css create mode 100644 web/src/pages/TicketDetailPage.tsx create mode 100644 web/src/pages/UsersPage.module.css create mode 100644 web/src/pages/UsersPage.tsx create mode 100644 web/src/router.tsx create mode 100644 web/src/styles/index.css create mode 100644 web/tsconfig.app.json create mode 100644 web/tsconfig.json create mode 100644 web/tsconfig.node.json create mode 100644 web/vite.config.ts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3cf6983 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Normaliza line endings para LF em todo o repositório +* text=auto eol=lf + +# Binários — nunca converter +*.db binary +*.sqlite binary +*.png binary +*.jpg binary +*.ico binary +*.woff binary +*.woff2 binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..046c7e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +# ─── Node.js ───────────────────────────────────────────────────────────────── +node_modules/ +.pnpm-store/ +.pnpm-debug.log* +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# ─── Build & Dist ───────────────────────────────────────────────────────────── +dist/ +build/ +out/ +.output/ + +# ─── Banco de Dados SQLite ──────────────────────────────────────────────────── +*.db +*.db-shm +*.db-wal +*.sqlite +*.sqlite3 + +# ─── Drizzle ORM ───────────────────────────────────────────────────────────── +drizzle/ + +# ─── Variáveis de Ambiente ──────────────────────────────────────────────────── +.env +.env.local +.env.*.local + +# ─── Nix ───────────────────────────────────────────────────────────────────── +.direnv/ +result +result-* + +# ─── IDEs & Editores ────────────────────────────────────────────────────────── +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# ─── Sistema Operacional ───────────────────────────────────────────────────── +.DS_Store +Thumbs.db + +# ─── Logs ──────────────────────────────────────────────────────────────────── +logs/ +*.log + +# ─── Cobertura de Testes ───────────────────────────────────────────────────── +coverage/ +.nyc_output/ \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..e2b6df7 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,35 @@ +# Histórico de Contexto (Sessão Atual) + +Este arquivo documenta as regras de negócio, correções e funcionalidades implementadas na última sessão, para que futuros agentes saibam exatamente o que foi feito no projeto (Helpdesk). + +## 1. Regras de Negócio e UX (Interface) + +- **Chat em Tempo Real**: + - Implementado Server-Sent Events (SSE) para atualização em tempo real de novos chamados, mudança de status e novas mensagens no chat. + - O `EventSource` no frontend envia o token de autenticação via *query string* (`?token=...`), e o backend (`auth.plugin.ts`) foi ajustado para validar esse token com o `@fastify/jwt` adequadamente. +- **Nomes Reais no Chat**: As mensagens na interface não exibem mais os placeholders "Agente #1". Elas mostram o nome real do autor da mensagem, baseado nos dados do banco (tabela `users`). +- **Dashboard e Cache**: + - Havia um bug onde o Dashboard não atualizava em tempo real porque o cache nativo do navegador para o `fetch` estava devolvendo respostas oxidadas. + - Isso foi resolvido adicionando `cache: 'no-store'` no cliente HTTP base em `web/src/lib/api.ts`. + +## 2. Permissões e Visibilidade (Requesters vs Agents/Admins) + +- **Zona de Perigo e Alteração de Status**: + - Solicitantes (`requesters`) NÃO veem a aba para alterar o status do chamado e NÃO veem a "Zona de Perigo" (deletar chamados). + - A opção de alterar o status de um chamado só aparece se o chamado já tiver um `assigneeId` (já tiver sido assumido por um agente). +- **Bloqueio de Chat**: + - Existe uma configuração de sistema (`system_settings.allowRequesterChat`) que pode ser alterada na aba de Configurações por admins. + - Se estiver desativada, a caixa de "Adicionar Resposta" fica escondida para os usuários com a role `requester`. + - Se o chamado for fechado ou resolvido, também ocorre um bloqueio automático no chat para os solicitantes. + +## 3. Notas Internas no Chat + +- **Segurança Reforçada**: + - Agentes podem enviar mensagens como "Notas Internas". + - A rota `GET /tickets/:id/comments` (`listCommentsHandler`) filtra comentários onde `isInternal === true` caso quem esteja fazendo a requisição seja um `requester`. Dessa forma, o backend jamais envia dados restritos para quem não deveria ver. + - Para que a comunicação funcione da equipe para o solicitante com o chat bloqueado, os agentes devem enviar suas mensagens com a opção "Nota Interna" **desmarcada**. Assim ela será classificada como pública e repassada via SSE ao solicitante. + +--- + +**Nota para futuros agentes**: +Qualquer alteração na comunicação entre a API e o React Query precisa respeitar as tags de invalidação configuradas no webhook SSE (`useTicketEvents.ts`). Não use cache agressivo no frontend com o React Query, pois o SSE já gerencia a invalidação ativa de chaves como `['tickets']` e `['stats']`. diff --git a/GIT_SETUP.sh b/GIT_SETUP.sh new file mode 100644 index 0000000..e1e1bd8 --- /dev/null +++ b/GIT_SETUP.sh @@ -0,0 +1,97 @@ +# ============================================================================ +# GUIA: Inicialização do Repositório Helpdesk +# Infraestrutura: Git local → Gitolite (controle de acesso) → cgit (web UI) +# ============================================================================ + + +# ─── 1. INICIALIZAÇÃO LOCAL ─────────────────────────────────────────────────── + +# Entre na pasta do projeto +cd /caminho/para/helpdesk + +# Inicializa o repositório Git com branch principal chamada 'main' +git init --initial-branch=main + +# Adiciona todos os arquivos da Fase 1 ao staging +git add \ + flake.nix \ + .gitignore \ + .gitattributes \ + pnpm-workspace.yaml \ + package.json \ + README.md \ + api/.gitkeep \ + web/.gitkeep + +# Primeiro commit — sinaliza início formal do projeto +git commit -m "chore: inicializa monorepo helpdesk + +- Adiciona ambiente Nix reprodutível (flake.nix) +- Configura workspaces pnpm (api, web) +- Define .gitignore e .gitattributes +- Documenta setup no README" + + +# ─── 2. CONFIGURAÇÃO DO GITOLITE ───────────────────────────────────────────── +# (Executado no servidor onde o Gitolite está instalado) + +# No servidor, edite gitolite.conf para declarar o novo repositório: +# +# repo helpdesk +# RW+ = seu-usuario +# R = @equipe-ti +# +# Faça commit e push nas configurações do Gitolite: +# git -C ~/gitolite-admin commit -am "feat: adiciona repositório helpdesk" +# git -C ~/gitolite-admin push + + +# ─── 3. CONECTA O REMOTE E FAZ O PUSH INICIAL ──────────────────────────────── + +# Adiciona o remote 'origin' apontando para o Gitolite +# (substitua 'git' pelo usuário e 'seu-servidor' pelo hostname/IP) +git remote add origin git@seu-servidor:helpdesk.git + +# Verifica se o remote foi configurado corretamente +git remote -v + +# Envia o branch 'main' para o servidor e define como upstream +git push --set-upstream origin main + + +# ─── 4. CONFIGURAÇÃO DO CGIT (exposição web) ───────────────────────────────── +# (Executado no servidor, no arquivo de configuração do cgit) +# Localização típica: /etc/cgitrc ou /etc/cgit/cgitrc + +# Adicione a entrada abaixo ao arquivo cgitrc: +# +# repo.url=helpdesk +# repo.name=Helpdesk TI +# repo.desc=Sistema Interno de Chamados de TI +# repo.path=/home/git/repositories/helpdesk.git +# repo.owner=equipe-ti +# repo.readme=HEAD:README.md ← renderiza o README na página do repo + + +# ─── 5. FLUXO DE TRABALHO DIÁRIO (para referência) ─────────────────────────── + +# Clonar o repositório em uma nova máquina +git clone git@seu-servidor:helpdesk.git +cd helpdesk +nix develop # ativa o ambiente isolado +pnpm install # instala dependências Node + +# Criar uma branch de feature +git checkout -b feat/nome-da-feature + +# Após desenvolver, commitar seguindo Conventional Commits +git add . +git commit -m "feat(tickets): adiciona rota de criação de chamado" + +# Enviar e abrir para revisão +git push origin feat/nome-da-feature + +# Merge na main após revisão +git checkout main +git merge --no-ff feat/nome-da-feature +git push origin main diff --git a/README.md b/README.md new file mode 100644 index 0000000..b51dd6e --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# 🛠️ Helpdesk TI — Sistema Interno de Chamados + +Sistema interno de gerenciamento de chamados de TI, desenvolvido para rodar em infraestrutura própria (on-premise). + +## Stack Tecnológica + +| Camada | Tecnologia | +|------------|-------------------------------------| +| Backend | Node.js 20 + Fastify | +| ORM | Drizzle ORM | +| Banco | SQLite (via better-sqlite3) | +| Frontend | React 18 + TypeScript + Vite | +| Ambiente | Nix Flakes (reprodutível) | +| Monorepo | pnpm Workspaces | +| VCS | Git (espelhado via Gitolite/cgit) | + +## Pré-requisitos + +- [Nix](https://nixos.org/download) com suporte a Flakes habilitado +- Git + +## Setup do Ambiente + +```bash +# 1. Clone o repositório +git clone git@git.devlucas.me:helpdesk.git +cd helpdesk + +# 2. Entre no shell de desenvolvimento (instala todas as dependências automaticamente) +nix develop + +# 3. Instale as dependências Node de todos os workspaces +pnpm install + +# 4. Rode as migrações do banco de dados +pnpm db:migrate + +# 5. Inicie o ambiente de desenvolvimento (api + web em paralelo) +pnpm dev +``` + +## Scripts Disponíveis + +| Comando | Descrição | +|-------------------|------------------------------------------------| +| `pnpm dev` | Inicia api (porta 3000) e web (porta 5173) | +| `pnpm dev:api` | Inicia apenas o servidor Fastify | +| `pnpm dev:web` | Inicia apenas o frontend Vite | +| `pnpm build` | Build de produção de ambos os pacotes | +| `pnpm db:migrate` | Executa migrações pendentes do Drizzle | +| `pnpm db:studio` | Abre o Drizzle Studio (GUI do banco) | + +## Estrutura do Repositório + +``` +helpdesk/ +├── flake.nix # Ambiente Nix reprodutível +├── pnpm-workspace.yaml # Configuração dos workspaces +├── package.json # Scripts raiz do monorepo +├── api/ # Backend Fastify + Drizzle + SQLite +│ ├── src/ +│ │ ├── db/ # Schema, migrações e conexão +│ │ ├── modules/ # Módulos de tickets, usuários, comentários +│ │ └── server.ts # Entry point do Fastify +│ └── package.json +└── web/ # Frontend React + TypeScript + Vite + ├── src/ + │ ├── components/ # Componentes reutilizáveis + │ ├── pages/ # Páginas (solicitante, agente) + │ └── main.tsx # Entry point do React + └── package.json +``` + +## Configuração de VCS (Gitolite) + +Ver seção de configuração do Gitolite no guia de infraestrutura. diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 0000000..466b1ac --- /dev/null +++ b/api/.env.example @@ -0,0 +1,23 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Helpdesk API — Variáveis de Ambiente +# Copie este arquivo para .env e ajuste os valores: +# cp .env.example .env +# ───────────────────────────────────────────────────────────────────────────── + +# Banco de Dados SQLite +# Caminho relativo à raiz do pacote api/ +DATABASE_URL=file:./helpdesk.db + +# Servidor HTTP +PORT=3000 +HOST=0.0.0.0 + +# Ambiente de execução +# Valores: development | production | test +NODE_ENV=development + +# Autenticação JWT +# Gere um valor seguro com: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" +JWT_SECRET=troque_por_um_segredo_forte_em_producao +JWT_EXPIRES_IN=7d + diff --git a/api/drizzle.config.ts b/api/drizzle.config.ts new file mode 100644 index 0000000..2a0ab52 --- /dev/null +++ b/api/drizzle.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from 'drizzle-kit'; + +/** + * drizzle.config.ts + * + * Configuração do drizzle-kit — usado pelos comandos: + * pnpm db:generate → lê o schema e gera arquivos SQL de migração + * pnpm db:migrate → aplica as migrações pendentes no banco + * pnpm db:studio → abre o Drizzle Studio (GUI web para o banco) + */ +export default defineConfig({ + // Dialeto do banco de dados + dialect: 'sqlite', + + // Fonte de verdade: onde está o schema Drizzle + schema: './src/db/schema.ts', + + // Onde os arquivos SQL de migração serão gerados e lidos + out: './src/db/migrations', + + dbCredentials: { + // Lê a URL do banco do arquivo .env + // Em produção, garanta que DATABASE_URL está definido no ambiente do servidor + url: process.env['DATABASE_URL'] ?? 'file:./helpdesk.db', + }, + + // Exibe SQL gerado no terminal durante `db:generate` (útil para code review) + verbose: true, + + // Aborta se detectar mudanças destrutivas acidentais (DROP TABLE, DROP COLUMN) + strict: true, +}); diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..0a11511 --- /dev/null +++ b/api/package.json @@ -0,0 +1,40 @@ +{ + "name": "@helpdesk/api", + "version": "1.0.0", + "description": "Backend do Sistema de Chamados de TI — Fastify + Drizzle + SQLite", + "private": true, + "type": "module", + "engines": { + "node": ">=20.0.0" + }, + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsc --project tsconfig.json", + "start": "node dist/server.js", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio" + }, + "dependencies": { + "@fastify/cors": "^9.0.1", + "@fastify/jwt": "^8.0.1", + "@fastify/sensible": "^5.6.0", + "bcryptjs": "^2.4.3", + "better-sqlite3": "^11.1.2", + "dotenv": "^16.4.5", + "drizzle-orm": "^0.31.4", + "fastify": "^4.28.1", + "fastify-plugin": "^4.5.1", + "fastify-type-provider-zod": "1.1.9", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/better-sqlite3": "^7.6.11", + "@types/node": "^20.14.11", + "drizzle-kit": "^0.22.8", + "pino-pretty": "^11.2.1", + "tsx": "^4.16.2", + "typescript": "^5.5.3" + } +} diff --git a/api/src/db/connection.ts b/api/src/db/connection.ts new file mode 100644 index 0000000..2982336 --- /dev/null +++ b/api/src/db/connection.ts @@ -0,0 +1,37 @@ +/** + * connection.ts + * + * Singleton de conexão com o banco de dados. + * + * Exporta uma única instância do Drizzle ORM configurada com: + * - better-sqlite3 como driver síncrono + * - WAL mode para concorrência + * - Foreign keys habilitadas + * - Schema tipado para autocompletar queries + * + * Uso em qualquer módulo da aplicação: + * import { db } from '@/db/connection' + */ + +import Database from 'better-sqlite3'; +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import * as schema from './schema.js'; + +const DATABASE_URL = process.env['DATABASE_URL'] ?? 'file:./helpdesk.db'; +const dbPath = DATABASE_URL.replace(/^file:/, ''); + +// ─── Criação do cliente SQLite ──────────────────────────────────────────────── +const sqlite = new Database(dbPath); + +// WAL mode: leituras não bloqueiam escritas (essencial para servidor web) +sqlite.pragma('journal_mode = WAL'); + +// Ativa integridade referencial (desativada por padrão no SQLite) +sqlite.pragma('foreign_keys = ON'); + +// ─── Instância Drizzle com schema completo ──────────────────────────────────── +// Passar o schema habilita db.query.tickets.findMany() com joins automáticos +export const db = drizzle(sqlite, { schema }); + +// Exporta o tipo para uso em funções que recebem `db` como parâmetro +export type Database = typeof db; diff --git a/api/src/db/migrate.ts b/api/src/db/migrate.ts new file mode 100644 index 0000000..7964ef6 --- /dev/null +++ b/api/src/db/migrate.ts @@ -0,0 +1,56 @@ +/** + * migrate.ts + * + * Script responsável por aplicar todas as migrations pendentes no banco SQLite. + * + * Pode ser executado de duas formas: + * 1. Manual (CLI): pnpm db:migrate + * 2. Programática: importado e chamado no boot do servidor (server.ts) + */ + +import Database from 'better-sqlite3'; +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import { migrate } from 'drizzle-orm/better-sqlite3/migrator'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// Resolve o diretório correto em módulos ESM (não existe __dirname por padrão) +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const MIGRATIONS_DIR = path.join(__dirname, 'migrations'); +const DATABASE_URL = process.env['DATABASE_URL'] ?? 'file:./helpdesk.db'; + +// Drizzle espera o caminho sem o prefixo "file:" +const dbPath = DATABASE_URL.replace(/^file:/, ''); + +export async function runMigrations(): Promise { + console.log('🗄️ Iniciando migrações do banco de dados...'); + console.log(` Banco : ${dbPath}`); + console.log(` Origem : ${MIGRATIONS_DIR}`); + + // better-sqlite3 opera de forma síncrona (sem Promises) + const sqlite = new Database(dbPath); + + // Otimizações de performance recomendadas para SQLite + sqlite.pragma('journal_mode = WAL'); // Write-Ahead Logging: leituras não bloqueiam escritas + sqlite.pragma('foreign_keys = ON'); // Ativa integridade referencial (desativada por padrão no SQLite) + + const db = drizzle(sqlite); + + // Aplica todas as migrations ainda não registradas na tabela __drizzle_migrations + migrate(db, { migrationsFolder: MIGRATIONS_DIR }); + + sqlite.close(); + console.log('✅ Migrações aplicadas com sucesso.'); +} + +// ─── Execução direta (CLI) ──────────────────────────────────────────────────── +// Roda apenas quando o arquivo é chamado diretamente (pnpm db:migrate) +// e não quando é importado como módulo pelo server.ts +const isMain = process.argv[1] === fileURLToPath(import.meta.url); + +if (isMain) { + runMigrations().catch((err) => { + console.error('❌ Falha ao aplicar migrações:', err); + process.exit(1); + }); +} diff --git a/api/src/db/migrations/0000_huge_luke_cage.sql b/api/src/db/migrations/0000_huge_luke_cage.sql new file mode 100644 index 0000000..1abb4d2 --- /dev/null +++ b/api/src/db/migrations/0000_huge_luke_cage.sql @@ -0,0 +1,61 @@ +CREATE TABLE `categories` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `slug` text NOT NULL, + `description` text, + `created_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL, + `updated_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL +); +--> statement-breakpoint +CREATE TABLE `comments` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `content` text NOT NULL, + `is_internal` integer DEFAULT false NOT NULL, + `ticket_id` integer NOT NULL, + `author_id` integer NOT NULL, + `created_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL, + `updated_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL, + FOREIGN KEY (`ticket_id`) REFERENCES `tickets`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE restrict +); +--> statement-breakpoint +CREATE TABLE `tickets` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `title` text NOT NULL, + `description` text NOT NULL, + `status` text DEFAULT 'open' NOT NULL, + `priority` text DEFAULT 'medium' NOT NULL, + `creator_id` integer NOT NULL, + `assignee_id` integer, + `category_id` integer, + `due_at` text, + `resolved_at` text, + `created_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL, + `updated_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL, + FOREIGN KEY (`creator_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE restrict, + FOREIGN KEY (`assignee_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE TABLE `users` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `email` text NOT NULL, + `password` text NOT NULL, + `role` text DEFAULT 'requester' NOT NULL, + `is_active` integer DEFAULT true NOT NULL, + `created_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL, + `updated_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `categories_slug_unique_idx` ON `categories` (`slug`);--> statement-breakpoint +CREATE INDEX `comments_ticket_id_idx` ON `comments` (`ticket_id`);--> statement-breakpoint +CREATE INDEX `comments_author_id_idx` ON `comments` (`author_id`);--> statement-breakpoint +CREATE INDEX `tickets_status_idx` ON `tickets` (`status`);--> statement-breakpoint +CREATE INDEX `tickets_priority_idx` ON `tickets` (`priority`);--> statement-breakpoint +CREATE INDEX `tickets_assignee_id_idx` ON `tickets` (`assignee_id`);--> statement-breakpoint +CREATE INDEX `tickets_creator_id_idx` ON `tickets` (`creator_id`);--> statement-breakpoint +CREATE INDEX `tickets_category_id_idx` ON `tickets` (`category_id`);--> statement-breakpoint +CREATE INDEX `tickets_status_assignee_idx` ON `tickets` (`status`,`assignee_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `users_email_unique_idx` ON `users` (`email`);--> statement-breakpoint +CREATE INDEX `users_role_idx` ON `users` (`role`); \ No newline at end of file diff --git a/api/src/db/migrations/0001_ambitious_emma_frost.sql b/api/src/db/migrations/0001_ambitious_emma_frost.sql new file mode 100644 index 0000000..c4256aa --- /dev/null +++ b/api/src/db/migrations/0001_ambitious_emma_frost.sql @@ -0,0 +1,6 @@ +CREATE TABLE `system_settings` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `allow_requester_chat` integer DEFAULT true NOT NULL, + `created_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL, + `updated_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL +); diff --git a/api/src/db/migrations/meta/0000_snapshot.json b/api/src/db/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..8cdfb93 --- /dev/null +++ b/api/src/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,446 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "6d22de90-25a5-4565-8b37-8d1d479c2dc6", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "categories": { + "name": "categories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "categories_slug_unique_idx": { + "name": "categories_slug_unique_idx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "comments": { + "name": "comments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_internal": { + "name": "is_internal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ticket_id": { + "name": "ticket_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "comments_ticket_id_idx": { + "name": "comments_ticket_id_idx", + "columns": [ + "ticket_id" + ], + "isUnique": false + }, + "comments_author_id_idx": { + "name": "comments_author_id_idx", + "columns": [ + "author_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "comments_ticket_id_tickets_id_fk": { + "name": "comments_ticket_id_tickets_id_fk", + "tableFrom": "comments", + "tableTo": "tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comments_author_id_users_id_fk": { + "name": "comments_author_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "tickets": { + "name": "tickets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'open'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'medium'" + }, + "creator_id": { + "name": "creator_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assignee_id": { + "name": "assignee_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "due_at": { + "name": "due_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "tickets_status_idx": { + "name": "tickets_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "tickets_priority_idx": { + "name": "tickets_priority_idx", + "columns": [ + "priority" + ], + "isUnique": false + }, + "tickets_assignee_id_idx": { + "name": "tickets_assignee_id_idx", + "columns": [ + "assignee_id" + ], + "isUnique": false + }, + "tickets_creator_id_idx": { + "name": "tickets_creator_id_idx", + "columns": [ + "creator_id" + ], + "isUnique": false + }, + "tickets_category_id_idx": { + "name": "tickets_category_id_idx", + "columns": [ + "category_id" + ], + "isUnique": false + }, + "tickets_status_assignee_idx": { + "name": "tickets_status_assignee_idx", + "columns": [ + "status", + "assignee_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tickets_creator_id_users_id_fk": { + "name": "tickets_creator_id_users_id_fk", + "tableFrom": "tickets", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "tickets_assignee_id_users_id_fk": { + "name": "tickets_assignee_id_users_id_fk", + "tableFrom": "tickets", + "tableTo": "users", + "columnsFrom": [ + "assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tickets_category_id_categories_id_fk": { + "name": "tickets_category_id_categories_id_fk", + "tableFrom": "tickets", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'requester'" + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "users_email_unique_idx": { + "name": "users_email_unique_idx", + "columns": [ + "email" + ], + "isUnique": true + }, + "users_role_idx": { + "name": "users_role_idx", + "columns": [ + "role" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/api/src/db/migrations/meta/0001_snapshot.json b/api/src/db/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..9fd41f7 --- /dev/null +++ b/api/src/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,486 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "e3832373-0ea8-46a1-8480-091761f1a94a", + "prevId": "6d22de90-25a5-4565-8b37-8d1d479c2dc6", + "tables": { + "categories": { + "name": "categories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "categories_slug_unique_idx": { + "name": "categories_slug_unique_idx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "comments": { + "name": "comments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_internal": { + "name": "is_internal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ticket_id": { + "name": "ticket_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "comments_ticket_id_idx": { + "name": "comments_ticket_id_idx", + "columns": [ + "ticket_id" + ], + "isUnique": false + }, + "comments_author_id_idx": { + "name": "comments_author_id_idx", + "columns": [ + "author_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "comments_ticket_id_tickets_id_fk": { + "name": "comments_ticket_id_tickets_id_fk", + "tableFrom": "comments", + "tableTo": "tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comments_author_id_users_id_fk": { + "name": "comments_author_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "system_settings": { + "name": "system_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "allow_requester_chat": { + "name": "allow_requester_chat", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "tickets": { + "name": "tickets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'open'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'medium'" + }, + "creator_id": { + "name": "creator_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assignee_id": { + "name": "assignee_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "due_at": { + "name": "due_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "tickets_status_idx": { + "name": "tickets_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "tickets_priority_idx": { + "name": "tickets_priority_idx", + "columns": [ + "priority" + ], + "isUnique": false + }, + "tickets_assignee_id_idx": { + "name": "tickets_assignee_id_idx", + "columns": [ + "assignee_id" + ], + "isUnique": false + }, + "tickets_creator_id_idx": { + "name": "tickets_creator_id_idx", + "columns": [ + "creator_id" + ], + "isUnique": false + }, + "tickets_category_id_idx": { + "name": "tickets_category_id_idx", + "columns": [ + "category_id" + ], + "isUnique": false + }, + "tickets_status_assignee_idx": { + "name": "tickets_status_assignee_idx", + "columns": [ + "status", + "assignee_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tickets_creator_id_users_id_fk": { + "name": "tickets_creator_id_users_id_fk", + "tableFrom": "tickets", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "tickets_assignee_id_users_id_fk": { + "name": "tickets_assignee_id_users_id_fk", + "tableFrom": "tickets", + "tableTo": "users", + "columnsFrom": [ + "assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tickets_category_id_categories_id_fk": { + "name": "tickets_category_id_categories_id_fk", + "tableFrom": "tickets", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'requester'" + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "users_email_unique_idx": { + "name": "users_email_unique_idx", + "columns": [ + "email" + ], + "isUnique": true + }, + "users_role_idx": { + "name": "users_role_idx", + "columns": [ + "role" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/api/src/db/migrations/meta/_journal.json b/api/src/db/migrations/meta/_journal.json new file mode 100644 index 0000000..b3ea0c0 --- /dev/null +++ b/api/src/db/migrations/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1784989322795, + "tag": "0000_huge_luke_cage", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1785005330713, + "tag": "0001_ambitious_emma_frost", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/api/src/db/schema.ts b/api/src/db/schema.ts new file mode 100644 index 0000000..ce95493 --- /dev/null +++ b/api/src/db/schema.ts @@ -0,0 +1,240 @@ +/** + * schema.ts + * + * Fonte única de verdade do banco de dados. + * Todos os tipos TypeScript são *inferidos* deste arquivo — nunca definidos + * manualmente — garantindo que DB e aplicação nunca fiquem dessincronizados. + */ + +import { sql } from 'drizzle-orm'; +import { + sqliteTable, + text, + integer, + index, + uniqueIndex, +} from 'drizzle-orm/sqlite-core'; + +// ───────────────────────────────────────────────────────────────────────────── +// HELPERS +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Colunas de auditoria reutilizáveis. + * SQLite armazena timestamps como texto ISO 8601. + */ +const timestamps = { + createdAt: text('created_at') + .notNull() + .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), + updatedAt: text('updated_at') + .notNull() + .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), +}; + +// ───────────────────────────────────────────────────────────────────────────── +// ENUMS (simulados via union types — SQLite não tem tipo ENUM nativo) +// ───────────────────────────────────────────────────────────────────────────── + +export const USER_ROLES = ['requester', 'agent', 'admin'] as const; +export type UserRole = (typeof USER_ROLES)[number]; + +export const TICKET_STATUSES = [ + 'open', // Chamado aberto, ainda não atribuído + 'in_progress', // Atribuído a um agente, em andamento + 'waiting', // Aguardando resposta do solicitante + 'resolved', // Solução aplicada, aguardando confirmação + 'closed', // Encerrado definitivamente +] as const; +export type TicketStatus = (typeof TICKET_STATUSES)[number]; + +export const TICKET_PRIORITIES = ['low', 'medium', 'high', 'critical'] as const; +export type TicketPriority = (typeof TICKET_PRIORITIES)[number]; + +// ───────────────────────────────────────────────────────────────────────────── +// TABELA: users +// ───────────────────────────────────────────────────────────────────────────── + +export const users = sqliteTable( + 'users', + { + id: integer('id').primaryKey({ autoIncrement: true }), + + // Identificação + name: text('name').notNull(), + email: text('email').notNull(), + password: text('password').notNull(), // hash bcrypt — nunca plain text + + // Controle de acesso + role: text('role', { enum: USER_ROLES }).notNull().default('requester'), + + // Conta ativa? Soft-delete sem perder histórico de tickets/comments + isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true), + + ...timestamps, + }, + (table) => ({ + // Garante emails únicos na camada do banco + emailUniqueIdx: uniqueIndex('users_email_unique_idx').on(table.email), + + // Consultas frequentes por role (ex: listar todos os agentes) + roleIdx: index('users_role_idx').on(table.role), + }), +); + +// ───────────────────────────────────────────────────────────────────────────── +// TABELA: categories +// ───────────────────────────────────────────────────────────────────────────── + +export const categories = sqliteTable( + 'categories', + { + id: integer('id').primaryKey({ autoIncrement: true }), + name: text('name').notNull(), + + // Slug para uso em URLs e filtros: "hardware", "acesso-sistema", etc. + slug: text('slug').notNull(), + + description: text('description'), + + ...timestamps, + }, + (table) => ({ + slugUniqueIdx: uniqueIndex('categories_slug_unique_idx').on(table.slug), + }), +); + +// ───────────────────────────────────────────────────────────────────────────── +// TABELA: tickets +// ───────────────────────────────────────────────────────────────────────────── + +export const tickets = sqliteTable( + 'tickets', + { + id: integer('id').primaryKey({ autoIncrement: true }), + + // Conteúdo + title: text('title').notNull(), + description: text('description').notNull(), + + // Classificação + status: text('status', { enum: TICKET_STATUSES }) + .notNull() + .default('open'), + + priority: text('priority', { enum: TICKET_PRIORITIES }) + .notNull() + .default('medium'), + + // ── Relacionamentos ──────────────────────────────────────────────────── + // Quem abriu o chamado (obrigatório) + creatorId: integer('creator_id') + .notNull() + .references(() => users.id, { onDelete: 'restrict' }), + + // Agente responsável (opcional — pode ser atribuído depois) + assigneeId: integer('assignee_id') + .references(() => users.id, { onDelete: 'set null' }), + + // Categoria do chamado (opcional — pode ser classificado depois) + categoryId: integer('category_id') + .references(() => categories.id, { onDelete: 'set null' }), + + // Data limite para resolução (SLA) + dueAt: text('due_at'), + + // Data em que o chamado foi efetivamente resolvido + resolvedAt: text('resolved_at'), + + ...timestamps, + }, + (table) => ({ + // Índices para os filtros mais comuns no painel do agente + statusIdx: index('tickets_status_idx').on(table.status), + priorityIdx: index('tickets_priority_idx').on(table.priority), + assigneeIdx: index('tickets_assignee_id_idx').on(table.assigneeId), + creatorIdx: index('tickets_creator_id_idx').on(table.creatorId), + categoryIdx: index('tickets_category_id_idx').on(table.categoryId), + + // Índice composto: painel do agente filtra por status + assignee com frequência + statusAssigneeIdx: index('tickets_status_assignee_idx').on( + table.status, + table.assigneeId, + ), + }), +); + +// ───────────────────────────────────────────────────────────────────────────── +// TABELA: comments +// ───────────────────────────────────────────────────────────────────────────── + +export const comments = sqliteTable( + 'comments', + { + id: integer('id').primaryKey({ autoIncrement: true }), + + content: text('content').notNull(), + + /** + * Diferencia mensagens públicas de notas internas da equipe. + * true → nota interna (visível apenas para agentes/admins) + * false → resposta pública (visível para o solicitante) + */ + isInternal: integer('is_internal', { mode: 'boolean' }) + .notNull() + .default(false), + + // ── Relacionamentos ──────────────────────────────────────────────────── + ticketId: integer('ticket_id') + .notNull() + .references(() => tickets.id, { onDelete: 'cascade' }), + + authorId: integer('author_id') + .notNull() + .references(() => users.id, { onDelete: 'restrict' }), + + ...timestamps, + }, + (table) => ({ + // Buscar todos os comentários de um ticket é a query mais frequente + ticketIdx: index('comments_ticket_id_idx').on(table.ticketId), + authorIdx: index('comments_author_id_idx').on(table.authorId), + }), +); + +// ───────────────────────────────────────────────────────────────────────────── +// TABELA: system_settings +// ───────────────────────────────────────────────────────────────────────────── + +export const systemSettings = sqliteTable( + 'system_settings', + { + id: integer('id').primaryKey({ autoIncrement: true }), + + // Configurações Globais + allowRequesterChat: integer('allow_requester_chat', { mode: 'boolean' }).notNull().default(true), + + ...timestamps, + } +); + +// ───────────────────────────────────────────────────────────────────────────── +// TIPOS INFERIDOS +// +// Use estes tipos em toda a aplicação. +// Ex: função que retorna um ticket → retorno tipado como `Ticket` +// ───────────────────────────────────────────────────────────────────────────── + +export type User = typeof users.$inferSelect; +export type NewUser = typeof users.$inferInsert; + +export type Category = typeof categories.$inferSelect; +export type NewCategory = typeof categories.$inferInsert; + +export type Ticket = typeof tickets.$inferSelect; +export type NewTicket = typeof tickets.$inferInsert; + +export type Comment = typeof comments.$inferSelect; +export type NewComment = typeof comments.$inferInsert; + +export type SystemSetting = typeof systemSettings.$inferSelect; diff --git a/api/src/db/seed.ts b/api/src/db/seed.ts new file mode 100644 index 0000000..2cde3ce --- /dev/null +++ b/api/src/db/seed.ts @@ -0,0 +1,60 @@ +import Database from 'better-sqlite3'; +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import * as schema from './schema.js'; +import bcrypt from 'bcryptjs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const DATABASE_URL = process.env['DATABASE_URL'] ?? 'file:./helpdesk.db'; +const dbPath = DATABASE_URL.replace(/^file:/, ''); + +const sqlite = new Database(dbPath); +const db = drizzle(sqlite, { schema }); + +async function seed() { + console.log('🌱 Verificando usuário admin...'); + + const existingAdmin = await db.query.users.findFirst({ + where: (users, { eq }) => eq(users.email, 'admin@helpdesk.local'), + }); + + if (!existingAdmin) { + const hashedPassword = await bcrypt.hash('admin123', 10); + + await db.insert(schema.users).values({ + name: 'Administrador TI', + email: 'admin@helpdesk.local', + password: hashedPassword, + role: 'admin', + isActive: true, + }); + + console.log('✅ Usuário admin criado com sucesso!'); + console.log('E-mail: admin@helpdesk.local'); + console.log('Senha: admin123'); + } else { + console.log('✅ Usuário admin já existe!'); + } + + console.log('🌱 Verificando categorias...'); + const existingCategories = await db.query.categories.findMany(); + if (existingCategories.length === 0) { + await db.insert(schema.categories).values([ + { name: 'Hardware', slug: 'hardware', description: 'Problemas físicos em computadores, impressoras e periféricos' }, + { name: 'Software', slug: 'software', description: 'Instalação, erros e licenças de programas' }, + { name: 'Redes e Internet', slug: 'redes', description: 'Sem conexão, lentidão ou falhas no Wi-Fi/Cabo' }, + { name: 'Acessos e Senhas', slug: 'acessos', description: 'Redefinição de senha, criação de e-mail e acessos a sistemas' }, + { name: 'Dúvidas e Suporte', slug: 'duvidas', description: 'Dúvidas gerais sobre uso de sistemas e equipamentos' } + ]); + console.log('✅ Categorias padrão criadas com sucesso!'); + } else { + console.log('✅ Categorias já existem!'); + } + + sqlite.close(); +} + +seed().catch((err) => { + console.error('❌ Erro no seed:', err); + process.exit(1); +}); diff --git a/api/src/modules/auth/auth.controller.ts b/api/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..8aec643 --- /dev/null +++ b/api/src/modules/auth/auth.controller.ts @@ -0,0 +1,114 @@ +/** + * auth.controller.ts + * Lógica de negócio de autenticação. + */ + +import bcrypt from 'bcryptjs'; +import type { FastifyReply, FastifyRequest } from 'fastify'; +import { + findUserByEmail, + findUserById, + createUser, +} from './auth.repository.js'; +import type { RegisterBody, LoginBody } from './auth.schema.js'; + +const SALT_ROUNDS = 12; + +// ─── POST /auth/register ────────────────────────────────────────────────────── + +export async function registerHandler( + request: FastifyRequest<{ Body: RegisterBody }>, + reply: FastifyReply, +) { + const { name, email, password, role } = request.body; + const db = request.server.db; + + // Verifica duplicidade de e-mail + const existing = await findUserByEmail(db, email); + if (existing) { + return reply.conflict('Este e-mail já está cadastrado.'); + } + + // Apenas admins podem criar agentes ou outros admins + if ((role === 'agent' || role === 'admin')) { + const callerUser = request.user as { role?: string } | undefined; + if (!callerUser || callerUser.role !== 'admin') { + return reply.forbidden('Apenas administradores podem criar agentes ou admins.'); + } + } + + // Hash da senha (bcrypt com salt 12 ≈ ~250ms — seguro e aceitável) + const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); + + const user = await createUser(db, { + name, + email, + password: hashedPassword, + role, + }); + + // Assina o JWT com o payload mínimo necessário + const token = await reply.jwtSign({ + id: user.id, + email: user.email, + role: user.role, + }); + + return reply.code(201).send({ token, user }); +} + +// ─── POST /auth/login ───────────────────────────────────────────────────────── + +export async function loginHandler( + request: FastifyRequest<{ Body: LoginBody }>, + reply: FastifyReply, +) { + const { email, password } = request.body; + const db = request.server.db; + + const user = await findUserByEmail(db, email); + + // Mensagem genérica: não revela se o e-mail existe ou não (segurança) + const INVALID_MSG = 'E-mail ou senha incorretos.'; + + if (!user) { + // Executa bcrypt mesmo sem usuário para evitar timing attacks + await bcrypt.compare(password, '$2b$12$invalidhashplaceholderthatnevermatchesanypassword'); + return reply.unauthorized(INVALID_MSG); + } + + if (!user.isActive) { + return reply.forbidden('Conta desativada. Entre em contato com o administrador.'); + } + + const isPasswordValid = await bcrypt.compare(password, user.password); + if (!isPasswordValid) { + return reply.unauthorized(INVALID_MSG); + } + + const token = await reply.jwtSign({ + id: user.id, + email: user.email, + role: user.role, + }); + + // Retorna o user sem o campo password + const { password: _pwd, ...safeUser } = user; + return reply.send({ token, user: safeUser }); +} + +// ─── GET /auth/me ───────────────────────────────────────────────────────────── + +export async function meHandler( + request: FastifyRequest, + reply: FastifyReply, +) { + // request.user foi injetado pelo decorator authenticate + const user = await findUserById(request.server.db, request.user.id); + + if (!user) { + return reply.unauthorized('Usuário não encontrado.'); + } + + return reply.send(user); +} diff --git a/api/src/modules/auth/auth.repository.ts b/api/src/modules/auth/auth.repository.ts new file mode 100644 index 0000000..25a931b --- /dev/null +++ b/api/src/modules/auth/auth.repository.ts @@ -0,0 +1,68 @@ +/** + * auth.repository.ts + * Queries de banco exclusivas do módulo de autenticação. + */ + +import { eq } from 'drizzle-orm'; +import type { Database } from '@/db/connection.js'; +import { users, type NewUser } from '@/db/schema.js'; + +// ─── Busca com senha (apenas para verificação no login) ─────────────────────── + +export async function findUserByEmail(db: Database, email: string) { + const [user] = await db + .select() + .from(users) + .where(eq(users.email, email)) + .limit(1); + + return user ?? null; +} + +// ─── Busca segura (sem password) ────────────────────────────────────────────── + +export async function findUserById(db: Database, id: number) { + const [user] = await db + .select({ + id: users.id, + name: users.name, + email: users.email, + role: users.role, + isActive: users.isActive, + createdAt: users.createdAt, + updatedAt: users.updatedAt, + }) + .from(users) + .where(eq(users.id, id)) + .limit(1); + + return user ?? null; +} + +// ─── Criação de usuário ─────────────────────────────────────────────────────── + +export async function createUser( + db: Database, + data: Pick, +) { + const now = new Date().toISOString(); + + const [user] = await db + .insert(users) + .values({ + ...data, + isActive: true, + createdAt: now, + updatedAt: now, + }) + .returning({ + id: users.id, + name: users.name, + email: users.email, + role: users.role, + isActive: users.isActive, + createdAt: users.createdAt, + }); + + return user!; +} diff --git a/api/src/modules/auth/auth.routes.ts b/api/src/modules/auth/auth.routes.ts new file mode 100644 index 0000000..1de0e16 --- /dev/null +++ b/api/src/modules/auth/auth.routes.ts @@ -0,0 +1,55 @@ +/** + * auth.routes.ts + * + * Rotas públicas e protegidas de autenticação: + * POST /api/v1/auth/register → cria conta + * POST /api/v1/auth/login → retorna JWT + * GET /api/v1/auth/me → dados do usuário logado (protegida) + */ + +import type { FastifyInstance } from 'fastify'; +import { + registerBodySchema, + loginBodySchema, +} from './auth.schema.js'; +import { + registerHandler, + loginHandler, + meHandler, +} from './auth.controller.js'; + +async function authRoutes(fastify: FastifyInstance): Promise { + + // ── POST /auth/register ──────────────────────────────────────────────────── + fastify.post('/register', { + schema: { + tags: ['Auth'], + summary: 'Registra um novo usuário', + body: registerBodySchema, + }, + handler: registerHandler, + }); + + // ── POST /auth/login ─────────────────────────────────────────────────────── + fastify.post('/login', { + schema: { + tags: ['Auth'], + summary: 'Autentica e retorna um JWT', + body: loginBodySchema, + }, + handler: loginHandler, + }); + + // ── GET /auth/me ─────────────────────────────────────────────────────────── + fastify.get('/me', { + schema: { + tags: ['Auth'], + summary: 'Retorna o usuário autenticado', + security: [{ bearerAuth: [] }], + }, + preHandler: [fastify.authenticate], + handler: meHandler, + }); +} + +export default authRoutes; diff --git a/api/src/modules/auth/auth.schema.ts b/api/src/modules/auth/auth.schema.ts new file mode 100644 index 0000000..7c9e8cd --- /dev/null +++ b/api/src/modules/auth/auth.schema.ts @@ -0,0 +1,66 @@ +/** + * auth.schema.ts + * Schemas Zod para registro, login e resposta de usuário. + */ + +import { z } from 'zod'; +import { USER_ROLES } from '@/db/schema.js'; + +// ─── Registro ───────────────────────────────────────────────────────────────── + +export const registerBodySchema = z.object({ + name: z + .string({ required_error: 'Nome é obrigatório' }) + .min(2, 'Nome deve ter no mínimo 2 caracteres') + .max(100) + .trim(), + + email: z + .string({ required_error: 'E-mail é obrigatório' }) + .email('E-mail inválido') + .toLowerCase() + .trim(), + + password: z + .string({ required_error: 'Senha é obrigatória' }) + .min(8, 'Senha deve ter no mínimo 8 caracteres') + .max(100), + + // Somente admins podem criar agentes/admins — validado no controller + role: z.enum(USER_ROLES).default('requester'), +}); + +export type RegisterBody = z.infer; + +// ─── Login ──────────────────────────────────────────────────────────────────── + +export const loginBodySchema = z.object({ + email: z + .string({ required_error: 'E-mail é obrigatório' }) + .email('E-mail inválido') + .toLowerCase() + .trim(), + + password: z.string({ required_error: 'Senha é obrigatória' }), +}); + +export type LoginBody = z.infer; + +// ─── Shape de resposta (NUNCA inclui password) ──────────────────────────────── + +export const userResponseSchema = z.object({ + id: z.number(), + name: z.string(), + email: z.string(), + role: z.enum(USER_ROLES), + isActive: z.boolean(), + createdAt: z.string(), +}); + +export const authResponseSchema = z.object({ + token: z.string(), + user: userResponseSchema, +}); + +export type UserResponse = z.infer; +export type AuthResponse = z.infer; diff --git a/api/src/modules/categories/categories.controller.ts b/api/src/modules/categories/categories.controller.ts new file mode 100644 index 0000000..9327932 --- /dev/null +++ b/api/src/modules/categories/categories.controller.ts @@ -0,0 +1,127 @@ +/** + * categories.controller.ts + * Lógica de negócio do CRUD de categorias. + */ + +import type { FastifyReply, FastifyRequest } from 'fastify'; +import { + findAllCategories, + findCategoryById, + findCategoryBySlug, + createCategory, + updateCategory, + deleteCategory, +} from './categories.repository.js'; +import type { + CategoryParams, + CreateCategoryBody, + UpdateCategoryBody, +} from './categories.schema.js'; +import { eq, count } from 'drizzle-orm'; +import { tickets } from '@/db/schema.js'; + +// ─── GET /categories ────────────────────────────────────────────────────────── + +export async function listCategoriesHandler( + request: FastifyRequest, + reply: FastifyReply, +) { + const list = await findAllCategories(request.server.db); + return reply.send(list); +} + +// ─── GET /categories/:id ────────────────────────────────────────────────────── + +export async function getCategoryHandler( + request: FastifyRequest<{ Params: CategoryParams }>, + reply: FastifyReply, +) { + const category = await findCategoryById(request.server.db, request.params.id); + + if (!category) { + return reply.notFound(`Categoria #${request.params.id} não encontrada.`); + } + + return reply.send(category); +} + +// ─── POST /categories ───────────────────────────────────────────────────────── + +export async function createCategoryHandler( + request: FastifyRequest<{ Body: CreateCategoryBody }>, + reply: FastifyReply, +) { + const { name, slug, description } = request.body; + const db = request.server.db; + + // Slug deve ser único + const existing = await findCategoryBySlug(db, slug); + if (existing) { + return reply.conflict(`Já existe uma categoria com o slug "${slug}".`); + } + + const category = await createCategory(db, { + name, + slug, + description: description ?? null, + }); + + return reply.code(201).send(category); +} + +// ─── PATCH /categories/:id ──────────────────────────────────────────────────── + +export async function updateCategoryHandler( + request: FastifyRequest<{ Params: CategoryParams; Body: UpdateCategoryBody }>, + reply: FastifyReply, +) { + const { id } = request.params; + const db = request.server.db; + + const existing = await findCategoryById(db, id); + if (!existing) { + return reply.notFound(`Categoria #${id} não encontrada.`); + } + + // Se o slug está sendo alterado, verifica conflito com outro registro + if (request.body.slug && request.body.slug !== existing.slug) { + const slugConflict = await findCategoryBySlug(db, request.body.slug); + if (slugConflict) { + return reply.conflict(`Já existe uma categoria com o slug "${request.body.slug}".`); + } + } + + const updated = await updateCategory(db, id, request.body); + return reply.send(updated); +} + +// ─── DELETE /categories/:id ─────────────────────────────────────────────────── + +export async function deleteCategoryHandler( + request: FastifyRequest<{ Params: CategoryParams }>, + reply: FastifyReply, +) { + const { id } = request.params; + const db = request.server.db; + + const existing = await findCategoryById(db, id); + if (!existing) { + return reply.notFound(`Categoria #${id} não encontrada.`); + } + + // Protege contra deleção de categoria em uso + const [{ total }] = await db + .select({ total: count() }) + .from(tickets) + .where(eq(tickets.categoryId, id)); + + if (total > 0) { + return reply.conflict( + `Categoria em uso por ${total} chamado(s). Reclassifique-os antes de excluir.`, + ); + } + + await deleteCategory(db, id); + + return reply.code(204).send(); +} diff --git a/api/src/modules/categories/categories.repository.ts b/api/src/modules/categories/categories.repository.ts new file mode 100644 index 0000000..1ca5901 --- /dev/null +++ b/api/src/modules/categories/categories.repository.ts @@ -0,0 +1,80 @@ +/** + * categories.repository.ts + * Queries de banco para o CRUD de categorias. + */ + +import { eq, asc } from 'drizzle-orm'; +import type { Database } from '@/db/connection.js'; +import { categories, type NewCategory } from '@/db/schema.js'; + +// ─── Leitura ────────────────────────────────────────────────────────────────── + +export async function findAllCategories(db: Database) { + return db + .select() + .from(categories) + .orderBy(asc(categories.name)); +} + +export async function findCategoryById(db: Database, id: number) { + const [cat] = await db + .select() + .from(categories) + .where(eq(categories.id, id)) + .limit(1); + + return cat ?? null; +} + +export async function findCategoryBySlug(db: Database, slug: string) { + const [cat] = await db + .select() + .from(categories) + .where(eq(categories.slug, slug)) + .limit(1); + + return cat ?? null; +} + +// ─── Criação ────────────────────────────────────────────────────────────────── + +export async function createCategory( + db: Database, + data: Pick, +) { + const now = new Date().toISOString(); + + const [category] = await db + .insert(categories) + .values({ ...data, createdAt: now, updatedAt: now }) + .returning(); + + return category!; +} + +// ─── Atualização ────────────────────────────────────────────────────────────── + +export async function updateCategory( + db: Database, + id: number, + data: Partial>, +) { + const [updated] = await db + .update(categories) + .set({ ...data, updatedAt: new Date().toISOString() }) + .where(eq(categories.id, id)) + .returning(); + + return updated ?? null; +} + +// ─── Exclusão ───────────────────────────────────────────────────────────────── + +export async function deleteCategory(db: Database, id: number) { + const [deleted] = await db + .delete(categories) + .where(eq(categories.id, id)) + .returning(); + + return deleted ?? null; +} diff --git a/api/src/modules/categories/categories.routes.ts b/api/src/modules/categories/categories.routes.ts new file mode 100644 index 0000000..cdf3602 --- /dev/null +++ b/api/src/modules/categories/categories.routes.ts @@ -0,0 +1,92 @@ +/** + * categories.routes.ts + * + * CRUD de categorias com controle de acesso por role: + * GET /api/v1/categories → lista todas (qualquer autenticado) + * GET /api/v1/categories/:id → busca por ID (qualquer autenticado) + * POST /api/v1/categories → cria (admin) + * PATCH /api/v1/categories/:id → atualiza (admin) + * DELETE /api/v1/categories/:id → exclui (admin) + */ + +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { + categoryParamsSchema, + createCategoryBodySchema, + updateCategoryBodySchema, +} from './categories.schema.js'; +import { + listCategoriesHandler, + getCategoryHandler, + createCategoryHandler, + updateCategoryHandler, + deleteCategoryHandler, +} from './categories.controller.js'; + +// ─── Guard de role ──────────────────────────────────────────────────────────── +// Verificação de permissão após autenticação +async function requireAdmin(request: FastifyRequest, reply: FastifyReply) { + if (request.user.role !== 'admin') { + await reply.forbidden('Apenas administradores podem gerenciar categorias.'); + } +} + +async function categoriesRoutes(fastify: FastifyInstance): Promise { + + // ── GET /categories ──────────────────────────────────────────────────────── + fastify.get('/', { + schema: { + tags: ['Categorias'], + summary: 'Lista todas as categorias', + }, + preHandler: [fastify.authenticate], + handler: listCategoriesHandler, + }); + + // ── GET /categories/:id ──────────────────────────────────────────────────── + fastify.get('/:id', { + schema: { + tags: ['Categorias'], + summary: 'Busca uma categoria pelo ID', + params: categoryParamsSchema, + }, + preHandler: [fastify.authenticate], + handler: getCategoryHandler, + }); + + // ── POST /categories ─────────────────────────────────────────────────────── + fastify.post('/', { + schema: { + tags: ['Categorias'], + summary: 'Cria uma nova categoria (admin)', + body: createCategoryBodySchema, + }, + preHandler: [fastify.authenticate, requireAdmin], + handler: createCategoryHandler, + }); + + // ── PATCH /categories/:id ────────────────────────────────────────────────── + fastify.patch('/:id', { + schema: { + tags: ['Categorias'], + summary: 'Atualiza uma categoria (admin)', + params: categoryParamsSchema, + body: updateCategoryBodySchema, + }, + preHandler: [fastify.authenticate, requireAdmin], + handler: updateCategoryHandler, + }); + + // ── DELETE /categories/:id ───────────────────────────────────────────────── + fastify.delete('/:id', { + schema: { + tags: ['Categorias'], + summary: 'Exclui uma categoria (admin)', + params: categoryParamsSchema, + }, + preHandler: [fastify.authenticate, requireAdmin], + handler: deleteCategoryHandler, + }); +} + +export default categoriesRoutes; diff --git a/api/src/modules/categories/categories.schema.ts b/api/src/modules/categories/categories.schema.ts new file mode 100644 index 0000000..6e11d8c --- /dev/null +++ b/api/src/modules/categories/categories.schema.ts @@ -0,0 +1,76 @@ +/** + * categories.schema.ts + * Schemas Zod para o CRUD de categorias. + */ + +import { z } from 'zod'; + +// ─── Helper: gerador de slug ────────────────────────────────────────────────── +// Converte "Hardware e Periféricos" → "hardware-e-perifericos" +function toSlug(name: string): string { + return name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') // remove acentos + .replace(/[^a-z0-9\s-]/g, '') // remove caracteres especiais + .trim() + .replace(/\s+/g, '-'); // espaços → hífens +} + +// ─── Criação ────────────────────────────────────────────────────────────────── + +export const createCategoryBodySchema = z.object({ + name: z + .string({ required_error: 'Nome é obrigatório' }) + .min(2, 'Nome deve ter no mínimo 2 caracteres') + .max(80) + .trim(), + + // Se não informado, será gerado automaticamente a partir do name + slug: z + .string() + .min(2) + .max(80) + .regex(/^[a-z0-9-]+$/, 'Slug deve conter apenas letras minúsculas, números e hífens') + .optional(), + + description: z.string().max(300).trim().optional(), +}).transform((data) => ({ + ...data, + // Garante que slug sempre exista — gerado do name se ausente + slug: data.slug ?? toSlug(data.name), +})); + +export type CreateCategoryBody = z.infer; + +// ─── Atualização (PATCH parcial) ────────────────────────────────────────────── + +export const updateCategoryBodySchema = z.object({ + name: z.string().min(2).max(80).trim().optional(), + slug: z.string().min(2).max(80) + .regex(/^[a-z0-9-]+$/, 'Slug inválido').optional(), + description: z.string().max(300).trim().nullable().optional(), +}); + +export type UpdateCategoryBody = z.infer; + +// ─── Parâmetros de rota ─────────────────────────────────────────────────────── + +export const categoryParamsSchema = z.object({ + id: z.coerce.number().int().positive(), +}); + +export type CategoryParams = z.infer; + +// ─── Resposta ───────────────────────────────────────────────────────────────── + +export const categoryResponseSchema = z.object({ + id: z.number(), + name: z.string(), + slug: z.string(), + description: z.string().nullable(), + createdAt: z.string(), + updatedAt: z.string(), +}); + +export type CategoryResponse = z.infer; diff --git a/api/src/modules/interactions/interactions.controller.ts b/api/src/modules/interactions/interactions.controller.ts new file mode 100644 index 0000000..5008272 --- /dev/null +++ b/api/src/modules/interactions/interactions.controller.ts @@ -0,0 +1,190 @@ +/** + * interactions.controller.ts + * + * Lógica de negócio para todas as interações sobre tickets. + * Orquestra o repository, aplica guards e formata respostas. + */ + +import type { FastifyReply, FastifyRequest } from 'fastify'; +import { findTicketById } from '@/modules/tickets/tickets.repository.js'; +import { + updateTicketStatus, + assignTicket, + updateTicketPriority, + addComment, + findCommentsByTicketId, +} from './interactions.repository.js'; +import { getSettings } from '@/modules/settings/settings.repository.js'; +import type { + TicketIdParams, + UpdateTicketStatusBody, + AssignTicketBody, + UpdateTicketPriorityBody, + AddCommentBody, +} from './interactions.schema.js'; + +// ─── Guard reutilizável ─────────────────────────────────────────────────────── +// Verifica se o ticket existe antes de qualquer operação + +async function assertTicketExists( + request: FastifyRequest, + reply: FastifyReply, + ticketId: number, +) { + const ticket = await findTicketById(request.server.db, ticketId); + if (!ticket) { + await reply.notFound(`Ticket #${ticketId} não encontrado`); + return null; + } + return ticket; +} + +// ─── PATCH /tickets/:ticketId/status ───────────────────────────────────────── + +export async function updateTicketStatusHandler( + request: FastifyRequest<{ Params: TicketIdParams; Body: UpdateTicketStatusBody }>, + reply: FastifyReply, +) { + const { ticketId } = request.params; + const { status, reason } = request.body; + const db = request.server.db; + + const ticket = await assertTicketExists(request, reply, ticketId); + if (!ticket) return; + + // Impede mover um ticket já fechado + if (ticket.status === 'closed') { + return reply.badRequest('Tickets fechados não podem ser reabertos por esta rota.'); + } + + const updated = await updateTicketStatus(db, ticketId, status); + + // Se um motivo foi fornecido, registra automaticamente como nota interna + if (reason && request.body.authorId) { + await addComment(db, { + ticketId, + authorId: (request.body as { authorId?: number }).authorId ?? 0, + content: `Status alterado para **${status}**. Motivo: ${reason}`, + isInternal: true, + }); + } + + if (updated) { + request.server.events.emit('ticket.updated', { + id: updated.id, + status: updated.status, + assigneeId: updated.assigneeId, + creatorId: ticket.creatorId, + authorId: request.user!.id, + }); + } + + return reply.send(updated); +} + +// ─── PATCH /tickets/:ticketId/assign ───────────────────────────────────────── + +export async function assignTicketHandler( + request: FastifyRequest<{ Params: TicketIdParams; Body: AssignTicketBody }>, + reply: FastifyReply, +) { + const { ticketId } = request.params; + const db = request.server.db; + + const ticket = await assertTicketExists(request, reply, ticketId); + if (!ticket) return; + + const updated = await assignTicket(db, ticketId, request.body.assigneeId); + + if (updated) { + request.server.events.emit('ticket.updated', { + id: updated.id, + status: updated.status, + assigneeId: updated.assigneeId, + creatorId: ticket.creatorId, + authorId: request.user!.id, + }); + } + + return reply.send(updated); +} + +// ─── PATCH /tickets/:ticketId/priority ─────────────────────────────────────── + +export async function updateTicketPriorityHandler( + request: FastifyRequest<{ Params: TicketIdParams; Body: UpdateTicketPriorityBody }>, + reply: FastifyReply, +) { + const { ticketId } = request.params; + + const ticket = await assertTicketExists(request, reply, ticketId); + if (!ticket) return; + + const updated = await updateTicketPriority( + request.server.db, + ticketId, + request.body.priority, + ); + + return reply.send(updated); +} + +// ─── POST /tickets/:ticketId/comments ──────────────────────────────────────── + +export async function addCommentHandler( + request: FastifyRequest<{ Params: TicketIdParams; Body: AddCommentBody }>, + reply: FastifyReply, +) { + const { ticketId } = request.params; + const { content, isInternal, authorId } = request.body; + + const ticket = await assertTicketExists(request, reply, ticketId); + if (!ticket) return; + + // Não permite adicionar comentários em tickets fechados ou resolvidos + if (ticket.status === 'closed' || ticket.status === 'resolved') { + return reply.badRequest('Não é possível comentar em um ticket fechado ou resolvido.'); + } + + // Verifica a configuração do sistema para bloqueio de chat de requesters + if (request.user?.role === 'requester') { + const settings = await getSettings(request.server.db); + if (!settings.allowRequesterChat) { + return reply.forbidden('O chat de chamados está desativado para usuários padrão no momento.'); + } + } + + const comment = await addComment(request.server.db, { + ticketId, + authorId, + content, + isInternal, + }); + + request.server.events.emit('comment.added', { + ticketId: comment.ticketId, + authorId: comment.authorId, + isInternal: comment.isInternal, + creatorId: ticket.creatorId, + assigneeId: ticket.assigneeId, + }); + + return reply.code(201).send(comment); +} + +// ─── GET /tickets/:ticketId/comments ───────────────────────────────────────── + +export async function listCommentsHandler( + request: FastifyRequest<{ Params: TicketIdParams }>, + reply: FastifyReply, +) { + const { ticketId } = request.params; + + const ticket = await assertTicketExists(request, reply, ticketId); + if (!ticket) return; + + const includeInternal = request.user!.role !== 'requester'; + const result = await findCommentsByTicketId(request.server.db, ticketId, includeInternal); + + return reply.send(result); +} diff --git a/api/src/modules/interactions/interactions.repository.ts b/api/src/modules/interactions/interactions.repository.ts new file mode 100644 index 0000000..1a32dc0 --- /dev/null +++ b/api/src/modules/interactions/interactions.repository.ts @@ -0,0 +1,135 @@ +/** + * interactions.repository.ts + * + * Queries de banco para todas as interações sobre tickets existentes. + * Única camada que toca o Drizzle neste módulo. + */ + +import { eq } from 'drizzle-orm'; +import type { Database } from '@/db/connection.js'; +import { tickets, comments, users } from '@/db/schema.js'; +import type { TicketStatus, TicketPriority } from '@/db/schema.js'; + +// ─── Tickets ────────────────────────────────────────────────────────────────── + +export async function updateTicketStatus( + db: Database, + ticketId: number, + status: TicketStatus, +) { + const now = new Date().toISOString(); + + const [updated] = await db + .update(tickets) + .set({ + status, + updatedAt: now, + // Registra o momento exato da resolução para métricas de SLA + resolvedAt: status === 'resolved' || status === 'closed' ? now : null, + }) + .where(eq(tickets.id, ticketId)) + .returning(); + + return updated ?? null; +} + +export async function assignTicket( + db: Database, + ticketId: number, + assigneeId: number, +) { + const [updated] = await db + .update(tickets) + .set({ + assigneeId, + // Atribuir um agente muda automaticamente o status de 'open' → 'in_progress' + status: 'in_progress', + updatedAt: new Date().toISOString(), + }) + .where(eq(tickets.id, ticketId)) + .returning(); + + return updated ?? null; +} + +export async function updateTicketPriority( + db: Database, + ticketId: number, + priority: TicketPriority, +) { + const [updated] = await db + .update(tickets) + .set({ + priority, + updatedAt: new Date().toISOString(), + }) + .where(eq(tickets.id, ticketId)) + .returning(); + + return updated ?? null; +} + +// ─── Comentários ────────────────────────────────────────────────────────────── + +export async function addComment( + db: Database, + data: { + ticketId: number; + authorId: number; + content: string; + isInternal: boolean; + }, +) { + const now = new Date().toISOString(); + + const [comment] = await db + .insert(comments) + .values({ + ...data, + createdAt: now, + updatedAt: now, + }) + .returning(); + + return comment; +} + +export async function findCommentsByTicketId( + db: Database, + ticketId: number, + // Filtro: agentes veem tudo, solicitantes só veem comentários públicos + includeInternal: boolean = true, +) { + const rows = await db + .select({ + id: comments.id, + ticketId: comments.ticketId, + authorId: comments.authorId, + content: comments.content, + isInternal: comments.isInternal, + createdAt: comments.createdAt, + updatedAt: comments.updatedAt, + authorName: users.name, + }) + .from(comments) + .leftJoin(users, eq(comments.authorId, users.id)) + .where(eq(comments.ticketId, ticketId)) + .orderBy(comments.createdAt); // Cronológico: mais antigos primeiro + + // Filtra notas internas em memória (conjunto pequeno por ticket) + return includeInternal + ? rows + : rows.filter((c) => !c.isInternal); +} + +export async function deleteComment( + db: Database, + commentId: number, +) { + const [deleted] = await db + .delete(comments) + .where(eq(comments.id, commentId)) + .returning(); + + return deleted ?? null; +} diff --git a/api/src/modules/interactions/interactions.routes.ts b/api/src/modules/interactions/interactions.routes.ts new file mode 100644 index 0000000..9487fcd --- /dev/null +++ b/api/src/modules/interactions/interactions.routes.ts @@ -0,0 +1,86 @@ +/** + * interactions.routes.ts + * + * Plugin Fastify com as rotas de interação sobre tickets. + * Registrado no server.ts com prefixo '/api/v1'. + */ + +import type { FastifyInstance } from 'fastify'; +import { + ticketIdParamsSchema, + updateTicketStatusBodySchema, + assignTicketBodySchema, + updateTicketPriorityBodySchema, + addCommentBodySchema, +} from './interactions.schema.js'; +import { + updateTicketStatusHandler, + assignTicketHandler, + updateTicketPriorityHandler, + addCommentHandler, + listCommentsHandler, +} from './interactions.controller.js'; + +async function interactionsRoutes(fastify: FastifyInstance): Promise { + + // ── PATCH /tickets/:ticketId/status ──────────────────────────────────────── + fastify.patch('/tickets/:ticketId/status', { + schema: { + tags: ['Interações'], + summary: 'Atualiza o status de um chamado', + params: ticketIdParamsSchema, + body: updateTicketStatusBodySchema, + }, + preHandler: [fastify.authenticate], + handler: updateTicketStatusHandler, + }); + + // ── PATCH /tickets/:ticketId/assign ──────────────────────────────────────── + fastify.patch('/tickets/:ticketId/assign', { + schema: { + tags: ['Interações'], + summary: 'Atribui ou reatribui um chamado a um agente', + params: ticketIdParamsSchema, + body: assignTicketBodySchema, + }, + preHandler: [fastify.authenticate], + handler: assignTicketHandler, + }); + + // ── PATCH /tickets/:ticketId/priority ────────────────────────────────────── + fastify.patch('/tickets/:ticketId/priority', { + schema: { + tags: ['Interações'], + summary: 'Altera a prioridade de um chamado', + params: ticketIdParamsSchema, + body: updateTicketPriorityBodySchema, + }, + preHandler: [fastify.authenticate], + handler: updateTicketPriorityHandler, + }); + + // ── POST /tickets/:ticketId/comments ─────────────────────────────────────── + fastify.post('/tickets/:ticketId/comments', { + schema: { + tags: ['Interações'], + summary: 'Adiciona um comentário (público ou nota interna) no chamado', + params: ticketIdParamsSchema, + body: addCommentBodySchema, + }, + preHandler: [fastify.authenticate], + handler: addCommentHandler, + }); + + // ── GET /tickets/:ticketId/comments ──────────────────────────────────────── + fastify.get('/tickets/:ticketId/comments', { + schema: { + tags: ['Interações'], + summary: 'Lista todos os comentários de um chamado', + params: ticketIdParamsSchema, + }, + preHandler: [fastify.authenticate], + handler: listCommentsHandler, + }); +} + +export default interactionsRoutes; diff --git a/api/src/modules/interactions/interactions.schema.ts b/api/src/modules/interactions/interactions.schema.ts new file mode 100644 index 0000000..59eec29 --- /dev/null +++ b/api/src/modules/interactions/interactions.schema.ts @@ -0,0 +1,101 @@ +/** + * interactions.schema.ts + * + * Schemas Zod para o módulo de interações: + * - Atualização de status de um ticket + * - Atribuição de agente + * - Adição de comentários / notas internas + */ + +import { z } from 'zod'; +import { TICKET_STATUSES, TICKET_PRIORITIES } from '@/db/schema.js'; + +// ─── Parâmetros de rota compartilhados ─────────────────────────────────────── + +export const ticketIdParamsSchema = z.object({ + ticketId: z.coerce.number().int().positive(), +}); + +export const commentIdParamsSchema = z.object({ + ticketId: z.coerce.number().int().positive(), + commentId: z.coerce.number().int().positive(), +}); + +export type TicketIdParams = z.infer; +export type CommentIdParams = z.infer; + +// ─── Atualização de status ──────────────────────────────────────────────────── + +export const updateTicketStatusBodySchema = z.object({ + status: z.enum(TICKET_STATUSES, { + errorMap: () => ({ + message: `Status deve ser um de: ${TICKET_STATUSES.join(', ')}`, + }), + }), + + // Motivo da mudança — obrigatório ao fechar ou resolver um chamado + reason: z.string().trim().max(500).optional(), +}); + +export type UpdateTicketStatusBody = z.infer; + +// ─── Atribuição de agente ───────────────────────────────────────────────────── + +export const assignTicketBodySchema = z.object({ + assigneeId: z + .number({ required_error: 'assigneeId é obrigatório' }) + .int() + .positive(), +}); + +export type AssignTicketBody = z.infer; + +// ─── Atualização de prioridade ──────────────────────────────────────────────── + +export const updateTicketPriorityBodySchema = z.object({ + priority: z.enum(TICKET_PRIORITIES, { + errorMap: () => ({ + message: `Prioridade deve ser uma de: ${TICKET_PRIORITIES.join(', ')}`, + }), + }), +}); + +export type UpdateTicketPriorityBody = z.infer; + +// ─── Adição de comentário / nota interna ───────────────────────────────────── + +export const addCommentBodySchema = z.object({ + content: z + .string({ required_error: 'Conteúdo é obrigatório' }) + .min(1, 'Comentário não pode ser vazio') + .max(5000, 'Comentário muito longo') + .trim(), + + /** + * true → nota interna (só agentes/admins veem) + * false → resposta pública (solicitante recebe notificação) + */ + isInternal: z.boolean().default(false), + + // Em produção viria do JWT; por ora, aceito no body + authorId: z + .number({ required_error: 'authorId é obrigatório' }) + .int() + .positive(), +}); + +export type AddCommentBody = z.infer; + +// ─── Shape de resposta dos comentários ─────────────────────────────────────── + +export const commentResponseSchema = z.object({ + id: z.number(), + content: z.string(), + isInternal: z.boolean(), + ticketId: z.number(), + authorId: z.number(), + createdAt: z.string(), + updatedAt: z.string(), +}); + +export type CommentResponse = z.infer; diff --git a/api/src/modules/settings/settings.controller.ts b/api/src/modules/settings/settings.controller.ts new file mode 100644 index 0000000..479f476 --- /dev/null +++ b/api/src/modules/settings/settings.controller.ts @@ -0,0 +1,24 @@ +import type { FastifyReply, FastifyRequest } from 'fastify'; +import { getSettings, updateSettings } from './settings.repository.js'; +import type { UpdateSettingsBody } from './settings.schema.js'; + +export async function getSettingsHandler( + request: FastifyRequest, + reply: FastifyReply, +) { + const settings = await getSettings(request.server.db); + return reply.send(settings); +} + +export async function updateSettingsHandler( + request: FastifyRequest<{ Body: UpdateSettingsBody }>, + reply: FastifyReply, +) { + // Apenas admins podem atualizar as configurações globais + if (request.user?.role !== 'admin') { + return reply.forbidden('Apenas administradores podem alterar configurações do sistema.'); + } + + const updated = await updateSettings(request.server.db, request.body); + return reply.send(updated); +} diff --git a/api/src/modules/settings/settings.repository.ts b/api/src/modules/settings/settings.repository.ts new file mode 100644 index 0000000..a3d38ba --- /dev/null +++ b/api/src/modules/settings/settings.repository.ts @@ -0,0 +1,26 @@ +import type { Database } from '@/db/connection.js'; +import { systemSettings } from '@/db/schema.js'; + +export async function getSettings(db: Database) { + let [settings] = await db.select().from(systemSettings).limit(1); + + if (!settings) { + [settings] = await db.insert(systemSettings).values({ allowRequesterChat: true }).returning(); + } + + return settings; +} + +export async function updateSettings(db: Database, data: { allowRequesterChat: boolean }) { + let [settings] = await db.select().from(systemSettings).limit(1); + + if (!settings) { + [settings] = await db.insert(systemSettings).values(data).returning(); + } else { + [settings] = await db.update(systemSettings) + .set({ allowRequesterChat: data.allowRequesterChat, updatedAt: new Date().toISOString() }) + .returning(); + } + + return settings; +} diff --git a/api/src/modules/settings/settings.routes.ts b/api/src/modules/settings/settings.routes.ts new file mode 100644 index 0000000..5ae97da --- /dev/null +++ b/api/src/modules/settings/settings.routes.ts @@ -0,0 +1,30 @@ +import type { FastifyInstance } from 'fastify'; +import { getSettingsHandler, updateSettingsHandler } from './settings.controller.js'; +import { SettingsSchema } from './settings.schema.js'; + +export default async function settingsRoutes(fastify: FastifyInstance) { + fastify.get( + '/', + { + schema: { + tags: ['Settings'], + summary: 'Obtém as configurações globais do sistema', + }, + preHandler: [fastify.authenticate], + }, + getSettingsHandler, + ); + + fastify.patch( + '/', + { + schema: { + tags: ['Settings'], + summary: 'Atualiza as configurações globais do sistema (apenas admin)', + body: SettingsSchema, + }, + preHandler: [fastify.authenticate], + }, + updateSettingsHandler, + ); +} diff --git a/api/src/modules/settings/settings.schema.ts b/api/src/modules/settings/settings.schema.ts new file mode 100644 index 0000000..cd3a19b --- /dev/null +++ b/api/src/modules/settings/settings.schema.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +export const SettingsSchema = z.object({ + allowRequesterChat: z.boolean(), +}); + +export type UpdateSettingsBody = z.infer; diff --git a/api/src/modules/stats/stats.controller.ts b/api/src/modules/stats/stats.controller.ts new file mode 100644 index 0000000..520f692 --- /dev/null +++ b/api/src/modules/stats/stats.controller.ts @@ -0,0 +1,15 @@ +/** + * stats.controller.ts + * Lógica do dashboard. + */ + +import type { FastifyReply, FastifyRequest } from 'fastify'; +import { getTicketsStats } from './stats.repository.js'; + +export async function getStatsHandler( + request: FastifyRequest, + reply: FastifyReply, +) { + const stats = await getTicketsStats(request.server.db); + return reply.send(stats); +} diff --git a/api/src/modules/stats/stats.repository.ts b/api/src/modules/stats/stats.repository.ts new file mode 100644 index 0000000..d5a543d --- /dev/null +++ b/api/src/modules/stats/stats.repository.ts @@ -0,0 +1,41 @@ +/** + * stats.repository.ts + * Queries para obter os totais agrupados. + */ + +import { sql } from 'drizzle-orm'; +import type { Database } from '@/db/connection.js'; +import { tickets } from '@/db/schema.js'; + +export async function getTicketsStats(db: Database) { + // Uma única query para agrupar as contagens por status + const rows = await db + .select({ + status: tickets.status, + count: sql`count(*)`, + }) + .from(tickets) + .groupBy(tickets.status); + + // Inicializa o mapa com 0 + const stats = { + totalOpen: 0, + totalInProgress: 0, + totalWaiting: 0, + totalResolved: 0, + totalClosed: 0, + }; + + for (const row of rows) { + if (row.status === 'open') stats.totalOpen = row.count; + if (row.status === 'in_progress') stats.totalInProgress = row.count; + if (row.status === 'waiting') stats.totalWaiting = row.count; + if (row.status === 'resolved') stats.totalResolved = row.count; + if (row.status === 'closed') stats.totalClosed = row.count; + } + + return { + ...stats, + totalPending: stats.totalOpen + stats.totalInProgress, + }; +} diff --git a/api/src/modules/stats/stats.routes.ts b/api/src/modules/stats/stats.routes.ts new file mode 100644 index 0000000..cf02c08 --- /dev/null +++ b/api/src/modules/stats/stats.routes.ts @@ -0,0 +1,22 @@ +/** + * stats.routes.ts + * + * GET /api/v1/stats → retorna agregados para o dashboard + */ + +import type { FastifyInstance } from 'fastify'; +import { statsResponseSchema } from './stats.schema.js'; +import { getStatsHandler } from './stats.controller.js'; + +async function statsRoutes(fastify: FastifyInstance): Promise { + fastify.get('/', { + schema: { + tags: ['Dashboard'], + summary: 'Retorna métricas e agregados dos chamados', + }, + preHandler: [fastify.authenticate], + handler: getStatsHandler, + }); +} + +export default statsRoutes; diff --git a/api/src/modules/stats/stats.schema.ts b/api/src/modules/stats/stats.schema.ts new file mode 100644 index 0000000..1616937 --- /dev/null +++ b/api/src/modules/stats/stats.schema.ts @@ -0,0 +1,17 @@ +/** + * stats.schema.ts + * Schemas para o módulo de estatísticas (Dashboard). + */ + +import { z } from 'zod'; + +export const statsResponseSchema = z.object({ + totalOpen: z.number(), + totalInProgress: z.number(), + totalWaiting: z.number(), + totalResolved: z.number(), + totalClosed: z.number(), + totalPending: z.number(), // open + in_progress +}); + +export type StatsResponse = z.infer; diff --git a/api/src/modules/tickets/tickets.controller.ts b/api/src/modules/tickets/tickets.controller.ts new file mode 100644 index 0000000..552974a --- /dev/null +++ b/api/src/modules/tickets/tickets.controller.ts @@ -0,0 +1,112 @@ +/** + * tickets.controller.ts + * + * Lógica de negócio do módulo de tickets. + * Orquestra o repository, aplica regras e formata respostas. + * NÃO acessa o banco diretamente — delega ao repository. + */ + +import type { FastifyReply, FastifyRequest } from 'fastify'; +import { createTicket, countTickets, findManyTickets } from './tickets.repository.js'; +import type { CreateTicketBody, ListTicketsQuery, TicketParams } from './tickets.schema.js'; +import { tickets } from '@/db/schema.js'; +import { eq } from 'drizzle-orm'; + +// ─── POST /tickets ──────────────────────────────────────────────────────────── + +export async function createTicketHandler( + request: FastifyRequest<{ Body: CreateTicketBody }>, + reply: FastifyReply, +) { + const { title, description, priority, categoryId, creatorId } = request.body; + + const ticket = await createTicket(request.server.db, { + title, + description, + priority, + categoryId: categoryId ?? null, + assigneeId: null, // Chamados nascem sem agente atribuído + creatorId, + status: 'open', + dueAt: null, + resolvedAt: null, + }); + + request.server.events.emit('ticket.created', { + id: ticket.id, + title: ticket.title, + creatorId: ticket.creatorId, + }); + + return reply.code(201).send(ticket); +} + +// ─── GET /tickets ───────────────────────────────────────────────────────────── + +export async function listTicketsHandler( + request: FastifyRequest<{ Querystring: ListTicketsQuery }>, + reply: FastifyReply, +) { + const filters = request.query; + const db = request.server.db; + + // Executa contagem e listagem em paralelo para não dobrar o tempo de resposta + const [total, data] = await Promise.all([ + countTickets(db, filters), + findManyTickets(db, filters), + ]); + + const totalPages = Math.ceil(total / filters.limit); + + return reply.send({ + data, + meta: { + page: filters.page, + limit: filters.limit, + total, + totalPages, + }, + }); +} + +// ─── GET /tickets/:id ───────────────────────────────────────────────────────── + +export async function getTicketByIdHandler( + request: FastifyRequest<{ Params: TicketParams }>, + reply: FastifyReply, +) { + const { findTicketById } = await import('./tickets.repository.js'); + const ticket = await findTicketById(request.server.db, request.params.id); + + if (!ticket) { + return reply.notFound(`Ticket #${request.params.id} não encontrado`); + } + + return reply.send(ticket); +} + +// ─── DELETE /tickets/:id ────────────────────────────────────────────────────── + +export async function deleteTicketHandler( + request: FastifyRequest<{ Params: TicketParams }>, + reply: FastifyReply, +) { + const db = request.server.db; + const { id } = request.params; + const callerUser = request.user!; + + const { findTicketById } = await import('./tickets.repository.js'); + const existingTicket = await findTicketById(db, id); + + if (!existingTicket) { + return reply.notFound('Chamado não encontrado.'); + } + + if (callerUser.role !== 'admin' && existingTicket.creatorId !== callerUser.id) { + return reply.forbidden('Você não tem permissão para excluir este chamado.'); + } + + await db.delete(tickets).where(eq(tickets.id, id)); + + return reply.code(204).send(); +} diff --git a/api/src/modules/tickets/tickets.repository.ts b/api/src/modules/tickets/tickets.repository.ts new file mode 100644 index 0000000..0c34abe --- /dev/null +++ b/api/src/modules/tickets/tickets.repository.ts @@ -0,0 +1,145 @@ +/** + * tickets.repository.ts + * + * Camada de acesso a dados do módulo de tickets. + * É a ÚNICA camada que importa e usa o Drizzle diretamente. + * + * Recebe `db` como parâmetro em vez de importar o singleton, + * o que facilita testes unitários com banco em memória. + */ + +import { eq, and, count, desc, type SQL } from 'drizzle-orm'; +import type { Database } from '@/db/connection.js'; +import { tickets, users, categories, type NewTicket } from '@/db/schema.js'; +import { alias } from 'drizzle-orm/sqlite-core'; +import type { ListTicketsQuery } from './tickets.schema.js'; + +// ─── Criação ────────────────────────────────────────────────────────────────── + +export async function createTicket( + db: Database, + data: NewTicket, +) { + const [ticket] = await db + .insert(tickets) + .values({ + ...data, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }) + .returning(); + + return ticket; +} + +// ─── Listagem com filtros e paginação ───────────────────────────────────────── + +export async function findManyTickets( + db: Database, + filters: ListTicketsQuery, +) { + const { status, priority, assigneeId, creatorId, categoryId, page, limit } = filters; + const offset = (page - 1) * limit; + + // Constrói array de condições dinamicamente + // Apenas condições com valor definido são incluídas no WHERE + const conditions: SQL[] = []; + if (status) conditions.push(eq(tickets.status, status)); + if (priority) conditions.push(eq(tickets.priority, priority)); + if (assigneeId) conditions.push(eq(tickets.assigneeId, assigneeId)); + if (creatorId) conditions.push(eq(tickets.creatorId, creatorId)); + if (categoryId) conditions.push(eq(tickets.categoryId, categoryId)); + + const where = conditions.length > 0 ? and(...conditions) : undefined; + + const creatorAlias = alias(users, 'creator'); + const assigneeAlias = alias(users, 'assignee'); + + const rows = await db + .select({ + id: tickets.id, + title: tickets.title, + description: tickets.description, + status: tickets.status, + priority: tickets.priority, + creatorId: tickets.creatorId, + assigneeId: tickets.assigneeId, + categoryId: tickets.categoryId, + dueAt: tickets.dueAt, + resolvedAt: tickets.resolvedAt, + createdAt: tickets.createdAt, + updatedAt: tickets.updatedAt, + creatorName: creatorAlias.name, + assigneeName: assigneeAlias.name, + categoryName: categories.name, + }) + .from(tickets) + .leftJoin(creatorAlias, eq(tickets.creatorId, creatorAlias.id)) + .leftJoin(assigneeAlias, eq(tickets.assigneeId, assigneeAlias.id)) + .leftJoin(categories, eq(tickets.categoryId, categories.id)) + .where(where) + .orderBy(desc(tickets.createdAt)) // Mais recentes primeiro + .limit(limit) + .offset(offset); + + return rows; +} + +// ─── Contagem (para meta de paginação) ─────────────────────────────────────── + +export async function countTickets( + db: Database, + filters: Omit, +) { + const { status, priority, assigneeId, creatorId, categoryId } = filters; + + const conditions: SQL[] = []; + if (status) conditions.push(eq(tickets.status, status)); + if (priority) conditions.push(eq(tickets.priority, priority)); + if (assigneeId) conditions.push(eq(tickets.assigneeId, assigneeId)); + if (creatorId) conditions.push(eq(tickets.creatorId, creatorId)); + if (categoryId) conditions.push(eq(tickets.categoryId, categoryId)); + + const where = conditions.length > 0 ? and(...conditions) : undefined; + + const [result] = await db + .select({ total: count() }) + .from(tickets) + .where(where); + + return result?.total ?? 0; +} + +// ─── Busca por ID ───────────────────────────────────────────────────────────── + +export async function findTicketById(db: Database, id: number) { + const creatorAlias = alias(users, 'creator'); + const assigneeAlias = alias(users, 'assignee'); + + const [ticket] = await db + .select({ + id: tickets.id, + title: tickets.title, + description: tickets.description, + status: tickets.status, + priority: tickets.priority, + creatorId: tickets.creatorId, + assigneeId: tickets.assigneeId, + categoryId: tickets.categoryId, + dueAt: tickets.dueAt, + resolvedAt: tickets.resolvedAt, + createdAt: tickets.createdAt, + updatedAt: tickets.updatedAt, + creatorName: creatorAlias.name, + assigneeName: assigneeAlias.name, + categoryName: categories.name, + }) + .from(tickets) + .leftJoin(creatorAlias, eq(tickets.creatorId, creatorAlias.id)) + .leftJoin(assigneeAlias, eq(tickets.assigneeId, assigneeAlias.id)) + .leftJoin(categories, eq(tickets.categoryId, categories.id)) + .where(eq(tickets.id, id)) + .limit(1); + + return ticket ?? null; +} diff --git a/api/src/modules/tickets/tickets.routes.ts b/api/src/modules/tickets/tickets.routes.ts new file mode 100644 index 0000000..69c04a4 --- /dev/null +++ b/api/src/modules/tickets/tickets.routes.ts @@ -0,0 +1,75 @@ +/** + * tickets.routes.ts + * + * Plugin Fastify com todas as rotas do módulo de tickets. + * Registrado no server.ts com prefixo '/api/v1/tickets'. + * + * Rotas disponíveis: + * POST /api/v1/tickets → abre novo chamado + * GET /api/v1/tickets → lista chamados (com filtros e paginação) + * GET /api/v1/tickets/:id → busca chamado por ID + */ + +import type { FastifyInstance } from 'fastify'; +import { + createTicketBodySchema, + listTicketsQuerySchema, + ticketParamsSchema, +} from './tickets.schema.js'; +import { + createTicketHandler, + listTicketsHandler, + getTicketByIdHandler, + deleteTicketHandler, +} from './tickets.controller.js'; + +async function ticketsRoutes(fastify: FastifyInstance): Promise { + + // ── POST /api/v1/tickets ────────────────────────────────────────────────── + fastify.post('/', { + schema: { + tags: ['Tickets'], + summary: 'Cria um novo chamado', + description: 'Cria um novo chamado, definindo o criador como o usuário logado.', + body: createTicketBodySchema, + }, + preHandler: [fastify.authenticate], + handler: createTicketHandler, + }); + + // ── GET /api/v1/tickets ─────────────────────────────────────────────────── + fastify.get('/', { + schema: { + tags: ['Tickets'], + summary: 'Lista os chamados', + description: 'Retorna a lista de chamados, suportando filtros e paginação.', + querystring: listTicketsQuerySchema, + }, + preHandler: [fastify.authenticate], + handler: listTicketsHandler, + }); + + // ── GET /api/v1/tickets/:id ─────────────────────────────────────────────── + fastify.get('/:id', { + schema: { + tags: ['Tickets'], + summary: 'Busca um chamado pelo ID', + params: ticketParamsSchema, + }, + preHandler: [fastify.authenticate], + handler: getTicketByIdHandler, + }); + + // ── DELETE /tickets/:id ──────────────────────────────────────────────────── + fastify.delete('/:id', { + schema: { + tags: ['Tickets'], + summary: 'Exclui um chamado pelo ID', + params: ticketParamsSchema, + }, + preHandler: [fastify.authenticate], + handler: deleteTicketHandler, + }); +} + +export default ticketsRoutes; diff --git a/api/src/modules/tickets/tickets.schema.ts b/api/src/modules/tickets/tickets.schema.ts new file mode 100644 index 0000000..43d94cf --- /dev/null +++ b/api/src/modules/tickets/tickets.schema.ts @@ -0,0 +1,89 @@ +/** + * tickets.schema.ts + * + * Schemas Zod para validação de entrada e saída do módulo de tickets. + * Usados tanto pelo Fastify (validação automática de requests) + * quanto pelo TypeScript (inferência de tipos). + */ + +import { z } from 'zod'; +import { TICKET_PRIORITIES, TICKET_STATUSES } from '@/db/schema.js'; + +// ─── Criação de Ticket ──────────────────────────────────────────────────────── + +export const createTicketBodySchema = z.object({ + title: z + .string({ required_error: 'Título é obrigatório' }) + .min(5, 'Título deve ter no mínimo 5 caracteres') + .max(150, 'Título deve ter no máximo 150 caracteres') + .trim(), + + description: z + .string({ required_error: 'Descrição é obrigatória' }) + .min(10, 'Descrição deve ter no mínimo 10 caracteres') + .trim(), + + priority: z.enum(TICKET_PRIORITIES).default('medium'), + + categoryId: z.number().int().positive().optional(), + + // Em uma implementação real, o creatorId viria do token JWT. + // Por ora, aceita no body para facilitar o desenvolvimento iterativo. + creatorId: z + .number({ required_error: 'creatorId é obrigatório' }) + .int() + .positive(), +}); + +export type CreateTicketBody = z.infer; + +// ─── Listagem de Tickets (filtros + paginação) ──────────────────────────────── + +export const listTicketsQuerySchema = z.object({ + status: z.enum(TICKET_STATUSES).optional(), + priority: z.enum(TICKET_PRIORITIES).optional(), + assigneeId: z.coerce.number().int().positive().optional(), + creatorId: z.coerce.number().int().positive().optional(), + categoryId: z.coerce.number().int().positive().optional(), + + // Paginação + page: z.coerce.number().int().min(1).default(1), + limit: z.coerce.number().int().min(1).max(100).default(20), +}); + +export type ListTicketsQuery = z.infer; + +// ─── Parâmetros de rota ─────────────────────────────────────────────────────── + +export const ticketParamsSchema = z.object({ + id: z.coerce.number().int().positive(), +}); + +export type TicketParams = z.infer; + +// ─── Shape de resposta ──────────────────────────────────────────────────────── + +export const ticketResponseSchema = z.object({ + id: z.number(), + title: z.string(), + description: z.string(), + status: z.enum(TICKET_STATUSES), + priority: z.enum(TICKET_PRIORITIES), + creatorId: z.number(), + assigneeId: z.number().nullable(), + categoryId: z.number().nullable(), + dueAt: z.string().nullable(), + resolvedAt: z.string().nullable(), + createdAt: z.string(), + updatedAt: z.string(), +}); + +export const listTicketsResponseSchema = z.object({ + data: z.array(ticketResponseSchema), + meta: z.object({ + page: z.number(), + limit: z.number(), + total: z.number(), + totalPages: z.number(), + }), +}); diff --git a/api/src/modules/users/users.controller.ts b/api/src/modules/users/users.controller.ts new file mode 100644 index 0000000..c3c06fd --- /dev/null +++ b/api/src/modules/users/users.controller.ts @@ -0,0 +1,129 @@ +import type { FastifyRequest, FastifyReply } from 'fastify'; +import bcrypt from 'bcryptjs'; +import { users } from '@/db/schema.js'; +import { eq, and, ne } from 'drizzle-orm'; +import type { CreateUserByAdminBody, UpdateUserBody } from './users.schema.js'; + +export async function listUsersHandler(request: FastifyRequest, reply: FastifyReply) { + const db = request.server.db; + const allUsers = await db.query.users.findMany({ + columns: { + password: false, // Don't return passwords + }, + orderBy: (users, { desc }) => [desc(users.createdAt)], + }); + + return reply.send(allUsers); +} + +export async function createUserHandler( + request: FastifyRequest<{ Body: CreateUserByAdminBody }>, + reply: FastifyReply, +) { + const db = request.server.db; + const { name, email, password, role } = request.body; + + // Verifica se email já existe + const existing = await db.query.users.findFirst({ + where: eq(users.email, email), + }); + + if (existing) { + return reply.conflict('Este e-mail já está cadastrado.'); + } + + const hashedPassword = await bcrypt.hash(password, 12); + + const [newUser] = await db.insert(users).values({ + name, + email, + password: hashedPassword, + role, + isActive: true, + }).returning({ + id: users.id, + name: users.name, + email: users.email, + role: users.role, + isActive: users.isActive, + createdAt: users.createdAt, + }); + + return reply.code(201).send(newUser); +} + +export async function updateUserHandler( + request: FastifyRequest<{ Params: { id: number }; Body: UpdateUserBody }>, + reply: FastifyReply, +) { + const db = request.server.db; + const { id } = request.params; + const { name, email, password, role, isActive } = request.body; + + const existingUser = await db.query.users.findFirst({ + where: eq(users.id, id), + }); + + if (!existingUser) { + return reply.notFound('Usuário não encontrado.'); + } + + if (email && email !== existingUser.email) { + const existingEmail = await db.query.users.findFirst({ + where: and(eq(users.email, email), ne(users.id, id)), + }); + if (existingEmail) { + return reply.conflict('Este e-mail já está em uso por outro usuário.'); + } + } + + const updates: Partial = { + name, + email, + role, + isActive, + updatedAt: new Date().toISOString(), + }; + + if (password && password.trim() !== '') { + updates.password = await bcrypt.hash(password, 12); + } + + const [updatedUser] = await db.update(users) + .set(updates) + .where(eq(users.id, id)) + .returning({ + id: users.id, + name: users.name, + email: users.email, + role: users.role, + isActive: users.isActive, + createdAt: users.createdAt, + }); + + return reply.send(updatedUser); +} + +export async function deleteUserHandler( + request: FastifyRequest<{ Params: { id: number } }>, + reply: FastifyReply, +) { + const db = request.server.db; + const { id } = request.params; + + const existingUser = await db.query.users.findFirst({ + where: eq(users.id, id), + }); + + if (!existingUser) { + return reply.notFound('Usuário não encontrado.'); + } + + if (id === request.user?.id) { + return reply.badRequest('Você não pode excluir a própria conta.'); + } + + await db.delete(users).where(eq(users.id, id)); + + return reply.code(204).send(); +} diff --git a/api/src/modules/users/users.routes.ts b/api/src/modules/users/users.routes.ts new file mode 100644 index 0000000..c7f1846 --- /dev/null +++ b/api/src/modules/users/users.routes.ts @@ -0,0 +1,56 @@ +import type { FastifyInstance } from 'fastify'; +import { createUserByAdminBodySchema, updateUserBodySchema, userIdParamsSchema } from './users.schema.js'; +import { listUsersHandler, createUserHandler, updateUserHandler, deleteUserHandler } from './users.controller.js'; + +export default async function usersRoutes(fastify: FastifyInstance) { + // Middleware customizado para garantir que apenas admins possam acessar + const requireAdmin = async (request: any, reply: any) => { + if (request.user?.role !== 'admin') { + return reply.forbidden('Apenas administradores podem gerenciar usuários.'); + } + }; + + // ── GET /users ───────────────────────────────────────────────────────────── + fastify.get('/', { + schema: { + tags: ['Users'], + summary: 'Lista todos os usuários (admin)', + }, + preHandler: [fastify.authenticate, requireAdmin], + handler: listUsersHandler, + }); + + // ── POST /users ──────────────────────────────────────────────────────────── + fastify.post('/', { + schema: { + tags: ['Users'], + summary: 'Cria um novo usuário (admin)', + body: createUserByAdminBodySchema, + }, + preHandler: [fastify.authenticate, requireAdmin], + handler: createUserHandler, + }); + + // ── PATCH /users/:id ─────────────────────────────────────────────────────── + fastify.patch('/:id', { + schema: { + tags: ['Users'], + summary: 'Atualiza um usuário (admin)', + params: userIdParamsSchema, + body: updateUserBodySchema, + }, + preHandler: [fastify.authenticate, requireAdmin], + handler: updateUserHandler, + }); + + // ── DELETE /users/:id ────────────────────────────────────────────────────── + fastify.delete('/:id', { + schema: { + tags: ['Users'], + summary: 'Exclui um usuário (admin)', + params: userIdParamsSchema, + }, + preHandler: [fastify.authenticate, requireAdmin], + handler: deleteUserHandler, + }); +} diff --git a/api/src/modules/users/users.schema.ts b/api/src/modules/users/users.schema.ts new file mode 100644 index 0000000..ff3f459 --- /dev/null +++ b/api/src/modules/users/users.schema.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; +import { USER_ROLES } from '@/db/schema.js'; + +export const createUserByAdminBodySchema = z.object({ + name: z.string().min(2).max(100).trim(), + email: z.string().email().toLowerCase().trim(), + password: z.string().min(8).max(100), + role: z.enum(USER_ROLES), +}); + +export type CreateUserByAdminBody = z.infer; + +export const updateUserBodySchema = z.object({ + name: z.string().min(2).max(100).trim().optional(), + email: z.string().email().toLowerCase().trim().optional(), + password: z.string().min(8).max(100).optional().or(z.literal('')), + role: z.enum(USER_ROLES).optional(), + isActive: z.boolean().optional(), +}); + +export type UpdateUserBody = z.infer; + +export const userIdParamsSchema = z.object({ + id: z.coerce.number().positive(), +}); diff --git a/api/src/plugins/auth.plugin.ts b/api/src/plugins/auth.plugin.ts new file mode 100644 index 0000000..0e6718a --- /dev/null +++ b/api/src/plugins/auth.plugin.ts @@ -0,0 +1,93 @@ +/** + * auth.plugin.ts + * + * Registra o @fastify/jwt e expõe dois decoradores: + * fastify.authenticate → rejeita requests sem token válido (401) + * fastify.optionalAuthenticate → injeta o usuário se houver token, mas não falha + * + * Uso nas rotas: + * { preHandler: [fastify.authenticate] } + */ + +import fp from 'fastify-plugin'; +import jwt from '@fastify/jwt'; +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; + +// ─── Payload do token ───────────────────────────────────────────────────────── + +export interface JwtPayload { + id: number; + email: string; + role: 'requester' | 'agent' | 'admin'; +} + +// ─── Augmentação de tipos Fastify ───────────────────────────────────────────── + +declare module 'fastify' { + interface FastifyInstance { + authenticate: (request: FastifyRequest, reply: FastifyReply) => Promise; + optionalAuthenticate: (request: FastifyRequest, reply: FastifyReply) => Promise; + } + interface FastifyRequest { + user: JwtPayload; + } +} + +declare module '@fastify/jwt' { + interface FastifyJWT { + payload: JwtPayload; + user: JwtPayload; + } +} + +// ─── Plugin ─────────────────────────────────────────────────────────────────── + +async function authPlugin(fastify: FastifyInstance): Promise { + const secret = process.env['JWT_SECRET']; + if (!secret) { + throw new Error('JWT_SECRET não definido. Verifique o arquivo .env'); + } + + await fastify.register(jwt, { + secret, + sign: { + expiresIn: process.env['JWT_EXPIRES_IN'] ?? '7d', + }, + }); + + // ── authenticate ──────────────────────────────────────────────────────────── + // Exige token válido — retorna 401 se ausente ou expirado + fastify.decorate( + 'authenticate', + async (request: FastifyRequest, reply: FastifyReply) => { + try { + // Suporte para SSE (EventSource não envia headers, envia via query string) + const query = request.query as { token?: string }; + if (query.token && !request.headers.authorization) { + request.headers.authorization = `Bearer ${query.token}`; + } + await request.jwtVerify(); + } catch { + await reply.unauthorized('Token inválido ou expirado. Faça login novamente.'); + } + }, + ); + + // ── optionalAuthenticate ──────────────────────────────────────────────────── + // Tenta verificar o token mas não falha se ausente + fastify.decorate( + 'optionalAuthenticate', + async (request: FastifyRequest, _reply: FastifyReply) => { + try { + await request.jwtVerify(); + } catch { + // Sem token → request.user fica undefined — trate no controller + } + }, + ); +} + +export default fp(authPlugin, { + name: 'auth-plugin', + fastify: '4.x', +}); diff --git a/api/src/plugins/db.plugin.ts b/api/src/plugins/db.plugin.ts new file mode 100644 index 0000000..47c82dd --- /dev/null +++ b/api/src/plugins/db.plugin.ts @@ -0,0 +1,31 @@ +/** + * db.plugin.ts + * + * Plugin Fastify que disponibiliza o banco de dados como decorador. + * + * Após registrar este plugin, qualquer rota pode acessar o banco via: + * fastify.db ou request.server.db + * + * O uso de fastify-plugin (fp) é essencial: sem ele, o Fastify encapsularia + * o decorador apenas no escopo do plugin, tornando-o invisível para outros plugins. + */ + +import fp from 'fastify-plugin'; +import type { FastifyInstance } from 'fastify'; +import { db, type Database } from '@/db/connection.js'; + +// Augmenta a tipagem do Fastify para incluir `db` com type-safety completo +declare module 'fastify' { + interface FastifyInstance { + db: Database; + } +} + +async function dbPlugin(fastify: FastifyInstance): Promise { + fastify.decorate('db', db); +} + +export default fp(dbPlugin, { + name: 'db-plugin', + fastify: '4.x', +}); diff --git a/api/src/plugins/events.plugin.ts b/api/src/plugins/events.plugin.ts new file mode 100644 index 0000000..fdf0e74 --- /dev/null +++ b/api/src/plugins/events.plugin.ts @@ -0,0 +1,34 @@ +/** + * events.plugin.ts + * + * Expõe um EventEmitter global no fastify para pub/sub em memória. + * Ideal para disparar Server-Sent Events (SSE) aos clientes conectados. + */ + +import fp from 'fastify-plugin'; +import { EventEmitter } from 'node:events'; +import type { FastifyInstance } from 'fastify'; + +export interface AppEventMap { + 'ticket.created': { id: number; title: string; creatorId: number }; + 'ticket.updated': { id: number; status: string; assigneeId: number | null; creatorId: number; authorId: number }; + 'comment.added': { ticketId: number; authorId: number; isInternal: boolean; creatorId: number; assigneeId: number | null }; +} + +declare module 'fastify' { + interface FastifyInstance { + events: EventEmitter; + } +} + +async function eventsPlugin(fastify: FastifyInstance): Promise { + const emitter = new EventEmitter(); + // Permite muitos clientes SSE conectados simultaneamente + emitter.setMaxListeners(100); + + fastify.decorate('events', emitter); +} + +export default fp(eventsPlugin, { + name: 'events-plugin', +}); diff --git a/api/src/plugins/events.routes.ts b/api/src/plugins/events.routes.ts new file mode 100644 index 0000000..fd14732 --- /dev/null +++ b/api/src/plugins/events.routes.ts @@ -0,0 +1,58 @@ +/** + * events.routes.ts + * + * Endpoint SSE (Server-Sent Events). + * Mantém a conexão aberta e repassa eventos do `fastify.events` para o cliente. + */ + +import fp from 'fastify-plugin'; +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; + +async function eventsRoutes(fastify: FastifyInstance): Promise { + fastify.get('/', { + schema: { + tags: ['Dashboard'], + summary: 'Stream de eventos em tempo real (SSE)', + }, + preHandler: [fastify.authenticate], + handler: (request: FastifyRequest, reply: FastifyReply) => { + // 1. Configura os headers para SSE + reply.raw.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }); + reply.raw.write('\n'); // Garante envio do header imediato + + // Helper para formatar pacote SSE + const sendEvent = (event: string, data: any) => { + reply.raw.write(`event: ${event}\n`); + reply.raw.write(`data: ${JSON.stringify(data)}\n\n`); + }; + + // 2. Registra os listeners + const onTicketCreated = (data: any) => sendEvent('ticket.created', data); + const onTicketUpdated = (data: any) => sendEvent('ticket.updated', data); + const onCommentAdded = (data: any) => sendEvent('comment.added', data); + + fastify.events.on('ticket.created', onTicketCreated); + fastify.events.on('ticket.updated', onTicketUpdated); + fastify.events.on('comment.added', onCommentAdded); + + // Ping a cada 30s para manter a conexão ativa (evitar timeout do LB/Nginx) + const pingInterval = setInterval(() => { + reply.raw.write(':\n\n'); // SSE comment block serve como heartbeat + }, 30000); + + // 3. Limpeza quando o cliente desconecta + request.raw.on('close', () => { + clearInterval(pingInterval); + fastify.events.off('ticket.created', onTicketCreated); + fastify.events.off('ticket.updated', onTicketUpdated); + fastify.events.off('comment.added', onCommentAdded); + }); + }, + }); +} + +export default eventsRoutes; diff --git a/api/src/script.js b/api/src/script.js new file mode 100644 index 0000000..320c0c4 --- /dev/null +++ b/api/src/script.js @@ -0,0 +1,5 @@ +const { default: bcrypt } = await import("bcryptjs"); + +const hashedPassword = await bcrypt.hash("12345678", 12); + +console.log(hashedPassword) \ No newline at end of file diff --git a/api/src/server.ts b/api/src/server.ts new file mode 100644 index 0000000..9ab75eb --- /dev/null +++ b/api/src/server.ts @@ -0,0 +1,115 @@ +/** + * server.ts + * + * Entry point do backend Helpdesk. + * + * Responsabilidades (em ordem): + * 1. Executar migrações do banco no boot + * 2. Criar e configurar a instância Fastify + * 3. Registrar plugins globais + * 4. Registrar módulos de rotas + * 5. Iniciar o servidor HTTP + */ + +import 'dotenv/config'; // Carrega .env antes de qualquer outro import +import Fastify from 'fastify'; +import cors from '@fastify/cors'; +import sensible from '@fastify/sensible'; +import { validatorCompiler } from 'fastify-type-provider-zod'; + +import { runMigrations } from '@/db/migrate.js'; +import dbPlugin from '@/plugins/db.plugin.js'; +import authPlugin from '@/plugins/auth.plugin.js'; +import authRoutes from '@/modules/auth/auth.routes.js'; +import ticketsRoutes from '@/modules/tickets/tickets.routes.js'; +import interactionsRoutes from '@/modules/interactions/interactions.routes.js'; +import categoriesRoutes from '@/modules/categories/categories.routes.js'; +import settingsRoutes from '@/modules/settings/settings.routes.js'; +import statsRoutes from '@/modules/stats/stats.routes.js'; +import usersRoutes from '@/modules/users/users.routes.js'; +import eventsPlugin from '@/plugins/events.plugin.js'; +import eventsRoutes from '@/plugins/events.routes.js'; + +// ─── Configuração ───────────────────────────────────────────────────────────── +const PORT = Number(process.env['PORT'] ?? 3000); +const HOST = process.env['HOST'] ?? '0.0.0.0'; +const IS_DEV = process.env['NODE_ENV'] !== 'production'; + +// ─── Factory da aplicação ───────────────────────────────────────────────────── +// Exportar como função facilita testes de integração (cada teste cria uma instância) +export async function buildApp() { + const fastify = Fastify({ + logger: { + level: IS_DEV ? 'info' : 'warn', + // Formatação legível no terminal em desenvolvimento + transport: IS_DEV + ? { target: 'pino-pretty', options: { colorize: true } } + : undefined, + }, + }); + + // Configura o Fastify para usar validação com Zod + fastify.setValidatorCompiler(validatorCompiler); + + // ── Plugins Globais ─────────────────────────────────────────────────────── + // CORS: permite requisições do frontend (Vite roda na porta 5173) + await fastify.register(cors, { + origin: IS_DEV ? 'http://localhost:5173' : process.env['FRONTEND_URL'], + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], + }); + + // Sensible: adiciona helpers como reply.notFound(), reply.badRequest(), etc. + await fastify.register(sensible); + + // Banco de dados: disponibiliza fastify.db em todas as rotas + await fastify.register(dbPlugin); + + // JWT: disponibiliza fastify.authenticate em todas as rotas + await fastify.register(authPlugin); + + // ── Plugins Locais ──────────────────────────────────────────────────────── + await fastify.register(eventsPlugin); + + // ── Rotas ───────────────────────────────────────────────────────────────── + // Auth: rotas públicas (sem authenticate) + await fastify.register(authRoutes, { prefix: '/api/v1/auth' }); + await fastify.register(statsRoutes, { prefix: '/api/v1/stats' }); + await fastify.register(eventsRoutes, { prefix: '/api/v1/events' }); + await fastify.register(categoriesRoutes, { prefix: '/api/v1/categories' }); + await fastify.register(settingsRoutes, { prefix: '/api/v1/settings' }); + await fastify.register(usersRoutes, { prefix: '/api/v1/users' }); + await fastify.register(ticketsRoutes, { prefix: '/api/v1/tickets' }); + await fastify.register(interactionsRoutes, { prefix: '/api/v1' }); + + // Health check — útil para monitoramento e load balancer + fastify.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() })); + + return fastify; +} + +// ─── Bootstrap ─────────────────────────────────────────────────────────────── +async function start(): Promise { + // 1. Aplica migrações pendentes antes de abrir o servidor para tráfego + await runMigrations(); + + // 2. Cria a instância configurada + const app = await buildApp(); + + // 3. Graceful shutdown — garante que requests em andamento terminem antes de fechar + const gracefulShutdown = async (signal: string) => { + app.log.info(`Sinal ${signal} recebido. Encerrando servidor...`); + await app.close(); + process.exit(0); + }; + + process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); // Kubernetes / systemd + process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Ctrl+C no terminal + + // 4. Inicia o servidor + await app.listen({ port: PORT, host: HOST }); +} + +start().catch((err) => { + console.error('❌ Falha ao iniciar o servidor:', err); + process.exit(1); +}); diff --git a/api/tsconfig.json b/api/tsconfig.json new file mode 100644 index 0000000..e11b6c1 --- /dev/null +++ b/api/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + // ── Compilação ──────────────────────────────────────────────────────────── + "target": "ES2022", // Node 20 suporta ES2022 nativamente + "module": "ESNext", // Saída em ESModules (alinhado com "type":"module") + "moduleResolution": "bundler", // Compatível com tsx (dev) e esbuild/tsc (prod) + "lib": ["ES2022"], + + // ── Saída ───────────────────────────────────────────────────────────────── + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, // Gera .d.ts para uso interno entre módulos + "sourceMap": true, // Facilita debug em desenvolvimento + + // ── Strict Mode (obrigatório para código limpo) ──────────────────────────── + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + + // ── Interoperabilidade ──────────────────────────────────────────────────── + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "verbatimModuleSyntax": true, // Força import type onde necessário + + // ── Path Aliases ────────────────────────────────────────────────────────── + // Permite imports como: import { db } from '@/db/connection' + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..378aa7f --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "helpdesk", + "version": "1.0.0", + "description": "Sistema Interno de Chamados de TI", + "private": true, + "engines": { + "node": ">=20.0.0", + "pnpm": ">=9.0.0" + }, + "scripts": { + "dev": "pnpm --parallel --filter @helpdesk/api --filter @helpdesk/web dev", + "dev:api": "pnpm --filter @helpdesk/api dev", + "dev:web": "pnpm --filter @helpdesk/web dev", + "build": "pnpm --filter @helpdesk/api build && pnpm --filter @helpdesk/web build", + "lint": "pnpm --recursive lint", + "db:generate":"pnpm --filter @helpdesk/api db:generate", + "db:migrate":"pnpm --filter @helpdesk/api db:migrate", + "db:studio": "pnpm --filter @helpdesk/api db:studio" + }, + "devDependencies": {} +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..58d0e0e --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4102 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + api: + dependencies: + '@fastify/cors': + specifier: ^9.0.1 + version: 9.0.1 + '@fastify/jwt': + specifier: ^8.0.1 + version: 8.0.1 + '@fastify/sensible': + specifier: ^5.6.0 + version: 5.6.0 + bcryptjs: + specifier: ^2.4.3 + version: 2.4.3 + better-sqlite3: + specifier: ^11.1.2 + version: 11.10.0 + dotenv: + specifier: ^16.4.5 + version: 16.6.1 + drizzle-orm: + specifier: ^0.31.4 + version: 0.31.4(@types/better-sqlite3@7.6.13)(@types/react@18.3.31)(better-sqlite3@11.10.0)(react@18.3.1) + fastify: + specifier: ^4.28.1 + version: 4.29.1 + fastify-plugin: + specifier: ^4.5.1 + version: 4.5.1 + fastify-type-provider-zod: + specifier: 1.1.9 + version: 1.1.9(fastify@4.29.1)(zod@3.25.76) + zod: + specifier: ^3.23.8 + version: 3.25.76 + devDependencies: + '@types/bcryptjs': + specifier: ^2.4.6 + version: 2.4.6 + '@types/better-sqlite3': + specifier: ^7.6.11 + version: 7.6.13 + '@types/node': + specifier: ^20.14.11 + version: 20.19.43 + drizzle-kit: + specifier: ^0.22.8 + version: 0.22.8 + pino-pretty: + specifier: ^11.2.1 + version: 11.3.0 + tsx: + specifier: ^4.16.2 + version: 4.23.1 + typescript: + specifier: ^5.5.3 + version: 5.9.3 + + web: + dependencies: + '@tanstack/react-query': + specifier: ^5.51.1 + version: 5.101.4(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-router-dom: + specifier: ^6.24.1 + version: 6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.3 + version: 18.3.31 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.31) + '@typescript-eslint/eslint-plugin': + specifier: ^7.16.0 + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^7.16.0 + version: 7.18.0(eslint@8.57.1)(typescript@5.9.3) + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.7.0(vite@5.4.21(@types/node@20.19.43)) + eslint: + specifier: ^8.57.0 + version: 8.57.1 + eslint-plugin-react-hooks: + specifier: ^4.6.2 + version: 4.6.2(eslint@8.57.1) + eslint-plugin-react-refresh: + specifier: ^0.4.7 + version: 0.4.26(eslint@8.57.1) + typescript: + specifier: ^5.5.3 + version: 5.9.3 + vite: + specifier: ^5.3.4 + version: 5.4.21(@types/node@20.19.43) + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild/aix-ppc64@0.19.12': + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.19.12': + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.19.12': + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.19.12': + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.19.12': + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.19.12': + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.19.12': + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.19.12': + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.19.12': + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.19.12': + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.19.12': + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.19.12': + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.19.12': + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.19.12': + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.19.12': + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.19.12': + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.19.12': + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.19.12': + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.19.12': + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.19.12': + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.19.12': + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.19.12': + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.19.12': + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@fastify/ajv-compiler@3.6.0': + resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==} + + '@fastify/cors@9.0.1': + resolution: {integrity: sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==} + + '@fastify/error@3.4.1': + resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} + + '@fastify/fast-json-stringify-compiler@4.3.0': + resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} + + '@fastify/jwt@8.0.1': + resolution: {integrity: sha512-295bd7V6bDCnZOu8MAQgM6r7V1KILB+kdEq1q6nbHfXCnML569n7NSo3WzeLDG6IAqDl+Rhzi1vjxwaNHhRCBA==} + + '@fastify/merge-json-schemas@0.1.1': + resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} + + '@fastify/sensible@5.6.0': + resolution: {integrity: sha512-Vq6Z2ZQy10GDqON+hvLF52K99s9et5gVVxTul5n3SIAf0Kq5QjPRUKkAMT3zPAiiGvoHtS3APa/3uaxfDgCODQ==} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@remix-run/router@1.23.3': + resolution: {integrity: sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==} + engines: {node: '>=14.0.0'} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@tanstack/query-core@5.101.4': + resolution: {integrity: sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==} + + '@tanstack/react-query@5.101.4': + resolution: {integrity: sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==} + peerDependencies: + react: ^18 || ^19 + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/bcryptjs@2.4.6': + resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} + + '@types/better-sqlite3@7.6.13': + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.31': + resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==} + + '@typescript-eslint/eslint-plugin@7.18.0': + resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@7.18.0': + resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@7.18.0': + resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/type-utils@7.18.0': + resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@7.18.0': + resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/typescript-estree@7.18.0': + resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@7.18.0': + resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/visitor-keys@7.18.0': + resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + avvio@8.4.0: + resolution: {integrity: sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.11.1: + resolution: {integrity: sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==} + engines: {node: '>=6.0.0'} + hasBin: true + + bcryptjs@2.4.3: + resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} + + better-sqlite3@11.10.0: + resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bn.js@4.12.5: + resolution: {integrity: sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==} + + brace-expansion@1.1.16: + resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} + + brace-expansion@2.1.2: + resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.7: + resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001806: + resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + drizzle-kit@0.22.8: + resolution: {integrity: sha512-VjI4wsJjk3hSqHSa3TwBf+uvH6M6pRHyxyoVbt935GUzP9tUR/BRZ+MhEJNgryqbzN2Za1KP0eJMTgKEPsalYQ==} + hasBin: true + + drizzle-orm@0.31.4: + resolution: {integrity: sha512-VGD9SH9aStF2z4QOTnVlVX/WghV/EnuEzTmsH3fSVp2E4fFgc8jl3viQrS/XUJx1ekW4rVVLJMH42SfGQdjX3Q==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=3' + '@electric-sql/pglite': '>=0.1.1' + '@libsql/client': '*' + '@neondatabase/serverless': '>=0.1' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/react': '>=18' + '@types/sql.js': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=13.2.0' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + react: '>=18' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/react': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + react: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + electron-to-chromium@1.5.396: + resolution: {integrity: sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@4.6.2: + resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + + eslint-plugin-react-refresh@0.4.26: + resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==} + peerDependencies: + eslint: '>=8.40' + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + fast-content-type-parse@1.1.0: + resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} + + fast-copy@3.0.2: + resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-json-stringify@5.16.1: + resolution: {integrity: sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==} + + fast-jwt@4.0.5: + resolution: {integrity: sha512-QnpNdn0955GT7SlT8iMgYfhTsityUWysrQjM+Q7bGFijLp6+TNWzlbSMPvgalbrQGRg4ZaHZgMcns5fYOm5avg==} + engines: {node: '>=16'} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-uri@2.4.3: + resolution: {integrity: sha512-8V8UrSDUkYpi4AXM7Na0G6hctXSaRHBGMuANOotuFdHEFtTdqDTRNfcDczA9WkKODI17o7o10iQvzdMIxXb8eA==} + + fast-uri@3.1.4: + resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} + + fastfall@1.5.1: + resolution: {integrity: sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==} + engines: {node: '>=0.10.0'} + + fastify-plugin@4.5.1: + resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} + + fastify-type-provider-zod@1.1.9: + resolution: {integrity: sha512-Ztnu1ZWJEKJouZvdTyfgjuVqS+A4JLoCbWBvFqFhfnrg6YQvEvW+5cJvP98kNbuV5gjfpWmHSOTi3BpkidJPQg==} + peerDependencies: + fastify: ^4.0.0 + zod: ^3.14.2 + + fastify@4.29.1: + resolution: {integrity: sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==} + + fastparallel@2.4.1: + resolution: {integrity: sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fastseries@1.7.2: + resolution: {integrity: sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-my-way@8.2.2: + resolution: {integrity: sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==} + engines: {node: '>=14'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.4.3: + resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + 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 + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-ref-resolver@1.0.1: + resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + light-my-request@5.14.0: + resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mnemonist@0.39.6: + resolution: {integrity: sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-abi@3.94.0: + resolution: {integrity: sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==} + engines: {node: '>=10'} + + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + engines: {node: '>=18'} + + obliterator@2.0.5: + resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-pretty@11.3.0: + resolution: {integrity: sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA==} + hasBin: true + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@9.14.0: + resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} + hasBin: true + + postcss@8.5.22: + resolution: {integrity: sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==} + engines: {node: ^10 || ^12 || >=14} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-router-dom@6.30.4: + resolution: {integrity: sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.30.4: + resolution: {integrity: sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + ret@0.4.3: + resolution: {integrity: sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==} + engines: {node: '>=10'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex2@3.1.0: + resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + steed@1.1.3: + resolution: {integrity: sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tar-fs@2.1.5: + resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thread-stream@3.2.0: + resolution: {integrity: sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toad-cache@3.7.4: + resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==} + engines: {node: '>=20'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + tsx@4.23.1: + resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.7 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.14.0 + + '@esbuild/aix-ppc64@0.19.12': + optional: true + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.19.12': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.19.12': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.19.12': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.19.12': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.19.12': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.19.12': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.19.12': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.19.12': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.19.12': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.19.12': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.19.12': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.19.12': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.19.12': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.19.12': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.19.12': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.19.12': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.19.12': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.19.12': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.19.12': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.19.12': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.19.12': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.19.12': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@fastify/ajv-compiler@3.6.0': + dependencies: + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + fast-uri: 2.4.3 + + '@fastify/cors@9.0.1': + dependencies: + fastify-plugin: 4.5.1 + mnemonist: 0.39.6 + + '@fastify/error@3.4.1': {} + + '@fastify/fast-json-stringify-compiler@4.3.0': + dependencies: + fast-json-stringify: 5.16.1 + + '@fastify/jwt@8.0.1': + dependencies: + '@fastify/error': 3.4.1 + '@lukeed/ms': 2.0.2 + fast-jwt: 4.0.5 + fastify-plugin: 4.5.1 + steed: 1.1.3 + + '@fastify/merge-json-schemas@0.1.1': + dependencies: + fast-deep-equal: 3.1.3 + + '@fastify/sensible@5.6.0': + dependencies: + '@lukeed/ms': 2.0.2 + fast-deep-equal: 3.1.3 + fastify-plugin: 4.5.1 + forwarded: 0.2.0 + http-errors: 2.0.1 + type-is: 1.6.18 + vary: 1.1.2 + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lukeed/ms@2.0.2': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@pinojs/redact@0.4.0': {} + + '@remix-run/router@1.23.3': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@tanstack/query-core@5.101.4': {} + + '@tanstack/react-query@5.101.4(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.101.4 + react: 18.3.1 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/bcryptjs@2.4.6': {} + + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 20.19.43 + + '@types/estree@1.0.9': {} + + '@types/node@20.19.43': + dependencies: + undici-types: 6.21.0 + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.31)': + dependencies: + '@types/react': 18.3.31 + + '@types/react@18.3.31': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 7.18.0 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3 + eslint: 8.57.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 8.57.1 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@7.18.0': {} + + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.9 + semver: 7.8.5 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) + eslint: 8.57.1 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.3.3': {} + + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@20.19.43))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21(@types/node@20.19.43) + transitivePeerDependencies: + - supports-color + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + abstract-logging@2.0.1: {} + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + ajv-formats@2.1.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.4 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + asn1.js@5.4.1: + dependencies: + bn.js: 4.12.5 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + + atomic-sleep@1.0.0: {} + + avvio@8.4.0: + dependencies: + '@fastify/error': 3.4.1 + fastq: 1.20.1 + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.11.1: {} + + bcryptjs@2.4.3: {} + + better-sqlite3@11.10.0: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bn.js@4.12.5: {} + + brace-expansion@1.1.16: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.7: + dependencies: + baseline-browser-mapping: 2.11.1 + caniuse-lite: 1.0.30001806 + electron-to-chromium: 1.5.396 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.7) + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001806: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chownr@1.1.4: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cookie@0.7.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + dateformat@4.6.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + depd@2.0.0: {} + + detect-libc@2.1.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dotenv@16.6.1: {} + + drizzle-kit@0.22.8: + dependencies: + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.19.12 + esbuild-register: 3.6.0(esbuild@0.19.12) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.31.4(@types/better-sqlite3@7.6.13)(@types/react@18.3.31)(better-sqlite3@11.10.0)(react@18.3.1): + optionalDependencies: + '@types/better-sqlite3': 7.6.13 + '@types/react': 18.3.31 + better-sqlite3: 11.10.0 + react: 18.3.1 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + electron-to-chromium@1.5.396: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + esbuild-register@3.6.0(esbuild@0.19.12): + dependencies: + debug: 4.4.3 + esbuild: 0.19.12 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.19.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-react-refresh@0.4.26(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.3 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.3.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + event-target-shim@5.0.1: {} + + events@3.3.0: {} + + expand-template@2.0.3: {} + + fast-content-type-parse@1.1.0: {} + + fast-copy@3.0.2: {} + + fast-decode-uri-component@1.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-json-stringify@5.16.1: + dependencies: + '@fastify/merge-json-schemas': 0.1.1 + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-deep-equal: 3.1.3 + fast-uri: 2.4.3 + json-schema-ref-resolver: 1.0.1 + rfdc: 1.4.1 + + fast-jwt@4.0.5: + dependencies: + '@lukeed/ms': 2.0.2 + asn1.js: 5.4.1 + ecdsa-sig-formatter: 1.0.11 + mnemonist: 0.39.6 + + fast-levenshtein@2.0.6: {} + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-safe-stringify@2.1.1: {} + + fast-uri@2.4.3: {} + + fast-uri@3.1.4: {} + + fastfall@1.5.1: + dependencies: + reusify: 1.1.0 + + fastify-plugin@4.5.1: {} + + fastify-type-provider-zod@1.1.9(fastify@4.29.1)(zod@3.25.76): + dependencies: + fastify: 4.29.1 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + + fastify@4.29.1: + dependencies: + '@fastify/ajv-compiler': 3.6.0 + '@fastify/error': 3.4.1 + '@fastify/fast-json-stringify-compiler': 4.3.0 + abstract-logging: 2.0.1 + avvio: 8.4.0 + fast-content-type-parse: 1.1.0 + fast-json-stringify: 5.16.1 + find-my-way: 8.2.2 + light-my-request: 5.14.0 + pino: 9.14.0 + process-warning: 3.0.0 + proxy-addr: 2.0.7 + rfdc: 1.4.1 + secure-json-parse: 2.7.0 + semver: 7.8.5 + toad-cache: 3.7.4 + + fastparallel@2.4.1: + dependencies: + reusify: 1.1.0 + xtend: 4.0.2 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fastseries@1.7.2: + dependencies: + reusify: 1.1.0 + xtend: 4.0.2 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + file-uri-to-path@1.0.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-my-way@8.2.2: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 3.1.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.4.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.4.3: {} + + forwarded@0.2.0: {} + + fs-constants@1.0.0: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + help-me@5.0.0: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ipaddr.js@1.9.1: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + isexe@2.0.0: {} + + joycon@3.1.1: {} + + js-tokens@4.0.0: {} + + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-ref-resolver@1.0.1: + dependencies: + fast-deep-equal: 3.1.3 + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + light-my-request@5.14.0: + dependencies: + cookie: 0.7.2 + process-warning: 3.0.0 + set-cookie-parser: 2.7.2 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + media-typer@0.3.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-response@3.1.0: {} + + minimalistic-assert@1.0.1: {} + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.16 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.2 + + minimist@1.2.8: {} + + mkdirp-classic@0.5.3: {} + + mnemonist@0.39.6: + dependencies: + obliterator: 2.0.5 + + ms@2.1.3: {} + + nanoid@3.3.16: {} + + napi-build-utils@2.0.0: {} + + natural-compare@1.4.0: {} + + node-abi@3.94.0: + dependencies: + semver: 7.8.5 + + node-releases@2.0.51: {} + + obliterator@2.0.5: {} + + on-exit-leak-free@2.1.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-pretty@11.3.0: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 3.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pump: 3.0.4 + readable-stream: 4.7.0 + secure-json-parse: 2.7.0 + sonic-boom: 4.2.1 + strip-json-comments: 3.1.1 + + pino-std-serializers@7.1.0: {} + + pino@9.14.0: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 3.2.0 + + postcss@8.5.22: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.94.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.5 + tunnel-agent: 0.6.0 + + prelude-ls@1.2.1: {} + + process-warning@3.0.0: {} + + process-warning@5.0.0: {} + + process@0.11.10: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + quick-format-unescaped@4.0.4: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-refresh@0.17.0: {} + + react-router-dom@6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.30.4(react@18.3.1) + + react-router@6.30.4(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.3 + react: 18.3.1 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + real-require@0.2.0: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + ret@0.4.3: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + safe-regex2@3.1.0: + dependencies: + ret: 0.4.3 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + secure-json-parse@2.7.0: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + set-cookie-parser@2.7.2: {} + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + slash@3.0.0: {} + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + split2@4.2.0: {} + + statuses@2.0.2: {} + + steed@1.1.3: + dependencies: + fastfall: 1.5.1 + fastparallel: 2.4.1 + fastq: 1.20.1 + fastseries: 1.7.2 + reusify: 1.1.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@2.0.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tar-fs@2.1.5: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + text-table@0.2.0: {} + + thread-stream@3.2.0: + dependencies: + real-require: 0.2.0 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toad-cache@3.7.4: {} + + toidentifier@1.0.1: {} + + ts-api-utils@1.4.3(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tsx@4.23.1: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.7): + dependencies: + browserslist: 4.28.7 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vary@1.1.2: {} + + vite@5.4.21(@types/node@20.19.43): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.22 + rollup: 4.62.2 + optionalDependencies: + '@types/node': 20.19.43 + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrappy@1.0.2: {} + + xtend@4.0.2: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..a65c96e --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +packages: + - "api" + - "web" +allowBuilds: + better-sqlite3: true + esbuild: true diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..982c17a --- /dev/null +++ b/web/index.html @@ -0,0 +1,27 @@ + + + + + + + + Helpdesk TI — Central de Chamados + + + + + + + + + + + + +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..9dba0fc --- /dev/null +++ b/web/package.json @@ -0,0 +1,37 @@ +{ + "name": "@helpdesk/web", + "version": "1.0.0", + "description": "Frontend do Sistema de Chamados de TI — React + TypeScript + Vite", + "private": true, + "type": "module", + "engines": { + "node": ">=20.0.0" + }, + + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + }, + + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.24.1", + "@tanstack/react-query": "^5.51.1" + }, + + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.5.3", + "vite": "^5.3.4", + "@typescript-eslint/eslint-plugin": "^7.16.0", + "@typescript-eslint/parser": "^7.16.0", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7" + } +} diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..e5bf00e --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,39 @@ +/** + * App.tsx — Raiz da aplicação com todos os providers globais. + */ + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { RouterProvider } from 'react-router-dom'; +import { AuthProvider } from './context/AuthContext'; +import { ToastProvider } from './context/ToastContext'; +import { router } from './router'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + gcTime: 5 * 60 * 1000, + retry: (failureCount, error) => { + if (error instanceof Error && 'status' in error) { + const status = (error as { status: number }).status; + if (status >= 400 && status < 500) return false; + } + return failureCount < 2; + }, + refetchOnWindowFocus: true, + }, + mutations: { retry: false }, + }, +}); + +export default function App() { + return ( + + + + + + + + ); +} diff --git a/web/src/components/auth/ProtectedRoute.tsx b/web/src/components/auth/ProtectedRoute.tsx new file mode 100644 index 0000000..1133d36 --- /dev/null +++ b/web/src/components/auth/ProtectedRoute.tsx @@ -0,0 +1,20 @@ +/** + * ProtectedRoute.tsx + * Redireciona para /login se o usuário não estiver autenticado. + */ + +import { Navigate, useLocation } from 'react-router-dom'; +import { useAuth } from '@/context/AuthContext'; +import type { ReactNode } from 'react'; + +export function ProtectedRoute({ children }: { children: ReactNode }) { + const { isAuthenticated } = useAuth(); + const location = useLocation(); + + if (!isAuthenticated) { + // Preserva a rota que o usuário tentou acessar para redirect pós-login + return ; + } + + return <>{children}; +} diff --git a/web/src/components/layout/AppLayout.module.css b/web/src/components/layout/AppLayout.module.css new file mode 100644 index 0000000..a6d32ce --- /dev/null +++ b/web/src/components/layout/AppLayout.module.css @@ -0,0 +1,194 @@ +.shell { + display: grid; + grid-template-columns: var(--sidebar-width) 1fr; + min-height: 100vh; +} + +/* ── Sidebar ───────────────────────────────────────────────────────────────── */ +.sidebar { + position: fixed; + top: 0; + left: 0; + width: var(--sidebar-width); + height: 100vh; + background: var(--color-surface); + border-right: 1px solid var(--color-border); + display: flex; + flex-direction: column; + z-index: 100; + overflow-y: auto; +} + +.sidebarHeader { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-5) var(--space-5); + border-bottom: 1px solid var(--color-border); +} + +.sidebarTitle { + font-size: var(--text-sm); + font-weight: 700; + color: var(--color-text-primary); + letter-spacing: -0.01em; + line-height: 1.2; +} + +.sidebarSubtitle { + font-size: var(--text-xs); + color: var(--color-text-muted); + margin-top: 1px; +} + +/* ── Navegação ─────────────────────────────────────────────────────────────── */ +.nav { + flex: 1; + padding: var(--space-4) var(--space-3); + display: flex; + flex-direction: column; + gap: var(--space-1); +} + +.navLabel { + font-size: var(--text-xs); + font-weight: 600; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; + padding: 0 var(--space-3); + margin-bottom: var(--space-2); +} + +.navLink { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-md); + font-size: var(--text-sm); + font-weight: 500; + color: var(--color-text-secondary); + transition: all var(--transition-fast); + text-decoration: none; +} + +.navLink:hover { + background: var(--color-surface-2); + color: var(--color-text-primary); +} + +.navLinkActive { + background: var(--color-accent-subtle); + color: var(--color-accent); +} + +.navLinkActive:hover { + background: var(--color-accent-subtle); + color: var(--color-accent-hover); +} + +/* ── Footer da sidebar ─────────────────────────────────────────────────────── */ +.sidebarFooter { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-4) var(--space-4); + border-top: 1px solid var(--color-border); + margin-top: auto; +} + +.userAvatar { + width: 32px; + height: 32px; + border-radius: var(--radius-full); + background: var(--color-accent-subtle); + color: var(--color-accent); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.userInfo { + min-width: 0; +} + +.userName { + font-size: var(--text-sm); + font-weight: 500; + color: var(--color-text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.userRole { + font-size: var(--text-xs); + color: var(--color-text-muted); +} + +.logoutBtn { + margin-left: auto; + flex-shrink: 0; + color: var(--color-text-muted); + padding: var(--space-1); + border-radius: var(--radius-sm); + transition: color var(--transition-fast), background var(--transition-fast); +} + +.logoutBtn:hover { + color: var(--color-priority-critical); + background: rgba(248, 113, 113, 0.1); +} + + +/* ── Área principal ────────────────────────────────────────────────────────── */ +.main { + grid-column: 2; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* ── Header ────────────────────────────────────────────────────────────────── */ +.header { + position: sticky; + top: 0; + height: var(--header-height); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--space-8); + background: rgba(15, 17, 23, 0.85); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--color-border); + z-index: 50; +} + +.pageTitle { + font-size: var(--text-lg); + font-weight: 600; + color: var(--color-text-primary); + letter-spacing: -0.02em; +} + +.headerActions { + display: flex; + align-items: center; + gap: var(--space-3); +} + +/* ── Conteúdo ──────────────────────────────────────────────────────────────── */ +.content { + flex: 1; + padding: var(--space-8); + max-width: var(--content-max-w); + width: 100%; + animation: fadeIn var(--transition-normal) ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} diff --git a/web/src/components/layout/AppLayout.tsx b/web/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..78a1795 --- /dev/null +++ b/web/src/components/layout/AppLayout.tsx @@ -0,0 +1,199 @@ +/** + * AppLayout.tsx + * + * Shell principal da aplicação: sidebar + header + área de conteúdo. + * Todas as páginas são renderizadas dentro deste layout via . + */ + +import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom'; +import { useAuth } from '@/context/AuthContext'; +import { useTicketEvents } from '@/hooks/useTicketEvents'; +import styles from './AppLayout.module.css'; + + +// ─── Ícones inline (SVG) ────────────────────────────────────────────────────── +const Icons = { + Dashboard: () => ( + + ), + Plus: () => ( + + ), + Ticket: () => ( + + ), + Logo: () => ( + + ), + User: () => ( + + ), + Users: () => ( + + ), + Settings: () => ( + + ), +}; + +// ─── Mapa de títulos por rota ───────────────────────────────────────────────── +const ROUTE_TITLES: Record = { + '/dashboard': 'Dashboard', + '/tickets/new': 'Abrir Chamado', + '/users': 'Usuários', +}; + +function getPageTitle(pathname: string): string { + if (ROUTE_TITLES[pathname]) return ROUTE_TITLES[pathname]; + if (pathname.startsWith('/tickets/')) return 'Detalhe do Chamado'; + return 'Helpdesk TI'; +} + +// ─── Componente ─────────────────────────────────────────────────────────────── +export default function AppLayout() { + const location = useLocation(); + const navigate = useNavigate(); + const { user, logout } = useAuth(); + const pageTitle = getPageTitle(location.pathname); + + // Inicializa o listener de eventos em tempo real + useTicketEvents(); + + function handleLogout() { + logout(); + navigate('/login', { replace: true }); + } + + + return ( +
+ + {/* ── Sidebar ─────────────────────────────────────────────────────── */} + + + {/* ── Área principal ───────────────────────────────────────────────── */} +
+ + {/* Header */} +
+

{pageTitle}

+
+ + + Novo Chamado + +
+
+ + {/* Conteúdo da página */} +
+ +
+
+
+ ); +} diff --git a/web/src/components/tickets/PriorityBadge.tsx b/web/src/components/tickets/PriorityBadge.tsx new file mode 100644 index 0000000..acd07a8 --- /dev/null +++ b/web/src/components/tickets/PriorityBadge.tsx @@ -0,0 +1,25 @@ +/** + * PriorityBadge.tsx + * Badge visual para a prioridade do ticket. + */ + +import type { TicketPriority } from '@/lib/api'; + +const PRIORITY_LABELS: Record = { + low: '↓ Baixa', + medium: '→ Média', + high: '↑ Alta', + critical: '⚡ Crítica', +}; + +interface PriorityBadgeProps { + priority: TicketPriority; +} + +export function PriorityBadge({ priority }: PriorityBadgeProps) { + return ( + + {PRIORITY_LABELS[priority]} + + ); +} diff --git a/web/src/components/tickets/StatusBadge.tsx b/web/src/components/tickets/StatusBadge.tsx new file mode 100644 index 0000000..e24876d --- /dev/null +++ b/web/src/components/tickets/StatusBadge.tsx @@ -0,0 +1,26 @@ +/** + * StatusBadge.tsx + * Badge visual para o status do ticket — usa classes do design system global. + */ + +import type { TicketStatus } from '@/lib/api'; + +const STATUS_LABELS: Record = { + open: 'Aberto', + in_progress: 'Em Andamento', + waiting: 'Aguardando', + resolved: 'Resolvido', + closed: 'Fechado', +}; + +interface StatusBadgeProps { + status: TicketStatus; +} + +export function StatusBadge({ status }: StatusBadgeProps) { + return ( + + {STATUS_LABELS[status]} + + ); +} diff --git a/web/src/components/tickets/TicketCard.module.css b/web/src/components/tickets/TicketCard.module.css new file mode 100644 index 0000000..494e3d3 --- /dev/null +++ b/web/src/components/tickets/TicketCard.module.css @@ -0,0 +1,94 @@ +.card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-left-width: 3px; + border-radius: var(--radius-lg); + padding: var(--space-4) var(--space-5); + cursor: pointer; + transition: all var(--transition-fast); + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.card:hover { + border-color: var(--color-border-hover); + border-left-color: inherit; + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} + +/* Borda esquerda colorida por prioridade */ +.priority_low { border-left-color: var(--color-priority-low); } +.priority_medium { border-left-color: var(--color-priority-medium); } +.priority_high { border-left-color: var(--color-priority-high); } +.priority_critical { border-left-color: var(--color-priority-critical); } + +.top { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-2); +} + +.badges { + display: flex; + gap: var(--space-2); + flex-wrap: wrap; +} + +.id { + font-size: var(--text-xs); + color: var(--color-text-muted); + font-variant-numeric: tabular-nums; + flex-shrink: 0; +} + +.title { + font-size: var(--text-base); + font-weight: 600; + color: var(--color-text-primary); + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.description { + font-size: var(--text-sm); + color: var(--color-text-secondary); + line-height: 1.6; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.footer { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: var(--space-1); + padding-top: var(--space-3); + border-top: 1px solid var(--color-border); +} + +.meta { + font-size: var(--text-xs); + color: var(--color-text-muted); +} + +.assigned { + display: flex; + align-items: center; + gap: 4px; + font-size: var(--text-xs); + color: var(--color-resolved); +} + +.unassigned { + font-size: var(--text-xs); + color: var(--color-text-muted); + font-style: italic; +} diff --git a/web/src/components/tickets/TicketCard.tsx b/web/src/components/tickets/TicketCard.tsx new file mode 100644 index 0000000..3b3926b --- /dev/null +++ b/web/src/components/tickets/TicketCard.tsx @@ -0,0 +1,66 @@ +/** + * TicketCard.tsx + * Card de ticket para uso na lista do dashboard. + */ + +import { useNavigate } from 'react-router-dom'; +import type { Ticket } from '@/lib/api'; +import { StatusBadge } from './StatusBadge'; +import { PriorityBadge } from './PriorityBadge'; +import styles from './TicketCard.module.css'; + +interface TicketCardProps { + ticket: Ticket; +} + +function formatDate(iso: string): string { + return new Intl.DateTimeFormat('pt-BR', { + day: '2-digit', + month: 'short', + hour: '2-digit', + minute: '2-digit', + }).format(new Date(iso)); +} + +export function TicketCard({ ticket }: TicketCardProps) { + const navigate = useNavigate(); + + return ( +
navigate(`/tickets/${ticket.id}`)} + role="button" + tabIndex={0} + onKeyDown={(e) => e.key === 'Enter' && navigate(`/tickets/${ticket.id}`)} + aria-label={`Ticket #${ticket.id}: ${ticket.title}`} + > +
+
+ + +
+ #{ticket.id} +
+ +

{ticket.title}

+ +

{ticket.description}

+ +
+ + Aberto em {formatDate(ticket.createdAt)} + + {ticket.assigneeId ? ( + + + Agente #{ticket.assigneeId} + + ) : ( + Sem agente + )} +
+
+ ); +} diff --git a/web/src/components/ui/PageLoader.module.css b/web/src/components/ui/PageLoader.module.css new file mode 100644 index 0000000..9683313 --- /dev/null +++ b/web/src/components/ui/PageLoader.module.css @@ -0,0 +1,26 @@ +.wrapper { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: var(--color-bg); + z-index: 9999; +} + +.content { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-5); +} + +.logo { + display: flex; + align-items: center; + gap: var(--space-3); + font-size: var(--text-lg); + font-weight: 600; + color: var(--color-text-primary); + letter-spacing: -0.02em; +} diff --git a/web/src/components/ui/PageLoader.tsx b/web/src/components/ui/PageLoader.tsx new file mode 100644 index 0000000..dcb34ab --- /dev/null +++ b/web/src/components/ui/PageLoader.tsx @@ -0,0 +1,28 @@ +/** + * PageLoader.tsx + * Tela de carregamento exibida enquanto uma rota lazy é importada. + */ + +import styles from './PageLoader.module.css'; + +export default function PageLoader() { + return ( +
+
+
+ + Helpdesk TI +
+
+
+
+ ); +} diff --git a/web/src/context/AuthContext.tsx b/web/src/context/AuthContext.tsx new file mode 100644 index 0000000..f86a940 --- /dev/null +++ b/web/src/context/AuthContext.tsx @@ -0,0 +1,79 @@ +/** + * AuthContext.tsx + * + * Contexto global de autenticação. + * Persiste o JWT no localStorage e expõe o estado do usuário logado. + */ + +import { + createContext, + useContext, + useState, + useCallback, + type ReactNode, +} from 'react'; +import type { UserResponse } from '@/lib/api'; + +// ─── Tipos ──────────────────────────────────────────────────────────────────── + +interface AuthState { + user: UserResponse | null; + token: string | null; + isAuthenticated: boolean; + login: (token: string, user: UserResponse) => void; + logout: () => void; +} + +// ─── Chave no localStorage ──────────────────────────────────────────────────── + +const TOKEN_KEY = 'helpdesk:token'; +const USER_KEY = 'helpdesk:user'; + +// ─── Context ────────────────────────────────────────────────────────────────── + +const AuthContext = createContext(null); + +// ─── Provider ───────────────────────────────────────────────────────────────── + +export function AuthProvider({ children }: { children: ReactNode }) { + // Inicializa com dados persistidos (restaura sessão ao recarregar) + const [token, setToken] = useState( + () => localStorage.getItem(TOKEN_KEY), + ); + const [user, setUser] = useState(() => { + const stored = localStorage.getItem(USER_KEY); + return stored ? (JSON.parse(stored) as UserResponse) : null; + }); + + const login = useCallback((newToken: string, newUser: UserResponse) => { + localStorage.setItem(TOKEN_KEY, newToken); + localStorage.setItem(USER_KEY, JSON.stringify(newUser)); + setToken(newToken); + setUser(newUser); + }, []); + + const logout = useCallback(() => { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(USER_KEY); + setToken(null); + setUser(null); + }, []); + + return ( + + {children} + + ); +} + +// ─── Hook ───────────────────────────────────────────────────────────────────── + +export function useAuth(): AuthState { + const ctx = useContext(AuthContext); + if (!ctx) { + throw new Error('useAuth deve ser usado dentro de '); + } + return ctx; +} diff --git a/web/src/context/Toast.module.css b/web/src/context/Toast.module.css new file mode 100644 index 0000000..bb56336 --- /dev/null +++ b/web/src/context/Toast.module.css @@ -0,0 +1,75 @@ +.toastContainer { + position: fixed; + bottom: var(--space-6); + right: var(--space-6); + display: flex; + flex-direction: column; + gap: var(--space-2); + z-index: 9999; + pointer-events: none; /* Deixa cliques passarem pelo container vazio */ +} + +.toast { + pointer-events: auto; /* Reativa cliques dentro do toast */ + min-width: 300px; + background: var(--color-surface); + color: var(--color-text-primary); + border: 1px solid var(--color-border); + padding: var(--space-3) var(--space-4); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + font-size: var(--text-sm); + font-weight: 500; + backdrop-filter: blur(8px); + + animation: slideIn var(--transition-normal) ease forwards; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(100%) scale(0.95); + } + to { + opacity: 1; + transform: translateX(0) scale(1); + } +} + +.info { + border-left: 4px solid var(--color-accent); +} + +.success { + border-left: 4px solid var(--color-priority-low); +} + +.error { + border-left: 4px solid var(--color-priority-critical); +} + +.closeBtn { + background: none; + border: none; + color: var(--color-text-muted); + font-size: var(--text-lg); + cursor: pointer; + padding: 0; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: var(--radius-sm); + transition: background var(--transition-fast), color var(--transition-fast); +} + +.closeBtn:hover { + background: var(--color-bg); + color: var(--color-text-primary); +} diff --git a/web/src/context/ToastContext.tsx b/web/src/context/ToastContext.tsx new file mode 100644 index 0000000..87b3894 --- /dev/null +++ b/web/src/context/ToastContext.tsx @@ -0,0 +1,71 @@ +/** + * ToastContext.tsx + * Sistema minimalista de Toast Notifications. + */ + +import { createContext, useContext, useState, useCallback, type ReactNode } from 'react'; +import { createPortal } from 'react-dom'; +import styles from './Toast.module.css'; + +type ToastType = 'info' | 'success' | 'error'; + +interface ToastData { + id: string; + message: string; + type: ToastType; +} + +interface ToastContextValue { + addToast: (message: string, type?: ToastType) => void; +} + +const ToastContext = createContext(null); + +export function ToastProvider({ children }: { children: ReactNode }) { + const [toasts, setToasts] = useState([]); + + const addToast = useCallback((message: string, type: ToastType = 'info') => { + const id = Math.random().toString(36).substring(2, 9); + setToasts((prev) => [...prev, { id, message, type }]); + + // Remove automaticamente após 4 segundos + setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, 4000); + }, []); + + const removeToast = (id: string) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }; + + return ( + + {children} + + {/* Portal renderiza os toasts no final do */} + {createPortal( +
+ {toasts.map((toast) => ( +
+ {toast.message} + +
+ ))} +
, + document.body + )} +
+ ); +} + +export function useToast() { + const ctx = useContext(ToastContext); + if (!ctx) throw new Error('useToast deve ser usado dentro do ToastProvider'); + return ctx; +} diff --git a/web/src/hooks/useTicketEvents.ts b/web/src/hooks/useTicketEvents.ts new file mode 100644 index 0000000..7994ace --- /dev/null +++ b/web/src/hooks/useTicketEvents.ts @@ -0,0 +1,91 @@ +/** + * useTicketEvents.ts + * + * Hook para escutar os Server-Sent Events (SSE) disparados pelo backend. + * Conecta via EventSource e despacha atualizações e invalidações de cache. + */ + +import { useEffect } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useAuth } from '@/context/AuthContext'; +import { useToast } from '@/context/ToastContext'; + +export function useTicketEvents() { + const { token, isAuthenticated, user } = useAuth(); + const queryClient = useQueryClient(); + const { addToast } = useToast(); + + useEffect(() => { + // Pede permissão para notificações nativas, se suportado + if ('Notification' in window && Notification.permission === 'default') { + Notification.requestPermission(); + } + }, []); + + useEffect(() => { + if (!isAuthenticated || !token) return; + + // Conecta ao endpoint SSE passando o token na query string + const url = `${import.meta.env['VITE_API_URL'] ?? '/api/v1'}/events?token=${token}`; + const eventSource = new EventSource(url); + + const notify = (title: string, body?: string) => { + // Toast notification + addToast(title, 'info'); + // Native notification + if ('Notification' in window && Notification.permission === 'granted') { + new Notification(title, { body, icon: '/favicon.ico' }); + } + }; + + // Quando um novo chamado é criado + eventSource.addEventListener('ticket.created', (e) => { + const data = JSON.parse(e.data); + if (user?.role === 'admin' || user?.role === 'agent') { + if (data.creatorId !== user.id) { + notify(`Novo chamado: #${data.id}`, data.title); + } + } + + // Invalida a lista para forçar o refetch e atualizar o grid + queryClient.invalidateQueries({ queryKey: ['tickets'] }); + queryClient.invalidateQueries({ queryKey: ['stats'] }); + }); + + // Quando o status ou responsável do chamado é atualizado + eventSource.addEventListener('ticket.updated', (e) => { + const data = JSON.parse(e.data); + if (user && (user.id === data.creatorId || user.id === data.assigneeId) && user.id !== data.authorId) { + notify(`Chamado #${data.id} atualizado`, `O status ou responsável foi alterado.`); + } + + // Atualiza as listas, os stats e o detalhe do ticket + queryClient.invalidateQueries({ queryKey: ['tickets'] }); + queryClient.invalidateQueries({ queryKey: ['stats'] }); + queryClient.invalidateQueries({ queryKey: ['ticket', data.id] }); + }); + + // Quando um comentário é adicionado + eventSource.addEventListener('comment.added', (e) => { + const data = JSON.parse(e.data); + if (user && (user.id === data.creatorId || user.id === data.assigneeId) && user.id !== data.authorId) { + if (!data.isInternal || (data.isInternal && user.role !== 'requester')) { + notify(`Nova mensagem no chamado #${data.ticketId}`, 'Você tem uma nova interação no chamado.'); + } + } + + // Atualiza os comentários do ticket afetado + queryClient.invalidateQueries({ queryKey: ['tickets', data.ticketId, 'comments'] }); + }); + + // Tratamento de erro na conexão + eventSource.onerror = (err) => { + console.error('Erro na conexão SSE:', err); + // O navegador tentará reconectar automaticamente + }; + + return () => { + eventSource.close(); + }; + }, [token, isAuthenticated, queryClient, addToast]); +} diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts new file mode 100644 index 0000000..1d1e63d --- /dev/null +++ b/web/src/lib/api.ts @@ -0,0 +1,297 @@ +/** + * api.ts + * + * Cliente HTTP tipado para a API Helpdesk. + * Centraliza toda comunicação com o backend — nenhum componente + * deve chamar fetch() diretamente. + */ + +// ─── Configuração ───────────────────────────────────────────────────────────── + +const BASE_URL = import.meta.env['VITE_API_URL'] ?? '/api/v1'; +const TOKEN_KEY = 'helpdesk:token'; + +function getToken(): string | null { + return localStorage.getItem(TOKEN_KEY); +} + + +// ─── Tipos (espelham o schema do backend) ───────────────────────────────────── + +export type TicketStatus = 'open' | 'in_progress' | 'waiting' | 'resolved' | 'closed'; +export type TicketPriority = 'low' | 'medium' | 'high' | 'critical'; + +export interface Category { + id: number; + name: string; + slug: string; + description: string | null; + createdAt: string; + updatedAt: string; +} + +export interface Ticket { + id: number; + title: string; + description: string; + status: TicketStatus; + priority: TicketPriority; + creatorId: number; + assigneeId: number | null; + categoryId: number | null; + dueAt: string | null; + resolvedAt: string | null; + createdAt: string; + updatedAt: string; + creatorName?: string; + assigneeName?: string | null; + categoryName?: string | null; +} + +export interface Comment { + id: number; + content: string; + isInternal: boolean; + ticketId: number; + authorId: number; + createdAt: string; + updatedAt: string; +} + +export interface PaginatedResponse { + data: T[]; + meta: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +} + +export interface SystemSettings { + allowRequesterChat: boolean; +} + +// ─── Filtros para listagem ──────────────────────────────────────────────────── + +export interface ListTicketsParams { + status?: TicketStatus; + priority?: TicketPriority; + assigneeId?: number; + creatorId?: number; + categoryId?: number; + page?: number; + limit?: number; +} + +// ─── Erro padronizado da API ────────────────────────────────────────────────── + +export class ApiError extends Error { + constructor( + public readonly status: number, + message: string, + ) { + super(message); + this.name = 'ApiError'; + } +} + +// ─── Função base de fetch ───────────────────────────────────────────────────── + +async function request( + path: string, + options?: RequestInit, +): Promise { + const token = getToken(); + const headers: Record = { ...options?.headers as Record }; + + if (options?.body && !headers['Content-Type']) { + headers['Content-Type'] = 'application/json'; + } + if (token && !headers['Authorization']) { + headers['Authorization'] = `Bearer ${token}`; + } + + const response = await fetch(`${BASE_URL}${path}`, { + cache: 'no-store', + ...options, + headers, + }); + + if (!response.ok) { + const body = await response.json().catch(() => ({ message: response.statusText })); + throw new ApiError( + response.status, + (body as { message?: string }).message ?? 'Erro desconhecido', + ); + } + + if (response.status === 204) return null as T; + + return response.json() as Promise; +} + +// ─── Tipos adicionais para Auth ─────────────────────────────────────────────── + +export interface UserResponse { + id: number; + name: string; + email: string; + role: 'requester' | 'agent' | 'admin'; + isActive: boolean; + createdAt: string; +} + +export interface AuthResponse { + token: string; + user: UserResponse; +} + +export interface StatsResponse { + totalOpen: number; + totalInProgress: number; + totalWaiting: number; + totalResolved: number; + totalClosed: number; + totalPending: number; +} + +// ─── Endpoints: Tickets ─────────────────────────────────────────────────────── + +export const api = { + settings: { + get(): Promise { + return request('/settings'); + }, + update(data: { allowRequesterChat: boolean }): Promise { + return request('/settings', { method: 'PATCH', body: JSON.stringify(data) }); + } + }, + + stats: { + get(): Promise { + return request('/stats'); + }, + }, + + auth: { + login(data: { email: string; password: string }): Promise { + return request('/auth/login', { method: 'POST', body: JSON.stringify(data) }); + }, + register(data: { name: string; email: string; password: string; role?: string }): Promise { + return request('/auth/register', { method: 'POST', body: JSON.stringify(data) }); + }, + me(): Promise { + return request('/auth/me'); + }, + }, + + categories: { + list(): Promise { + return request('/categories'); + }, + get(id: number): Promise { + return request(`/categories/${id}`); + }, + create(data: { name: string; slug?: string; description?: string }): Promise { + return request('/categories', { method: 'POST', body: JSON.stringify(data) }); + }, + update(id: number, data: { name?: string; slug?: string; description?: string }): Promise { + return request(`/categories/${id}`, { method: 'PATCH', body: JSON.stringify(data) }); + }, + delete(id: number): Promise { + return request(`/categories/${id}`, { method: 'DELETE' }); + } + }, + + tickets: { + list(params: ListTicketsParams = {}): Promise> { + const query = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined) query.set(key, String(value)); + }); + const qs = query.toString(); + return request(`/tickets${qs ? `?${qs}` : ''}`); + }, + + get(id: number): Promise { + return request(`/tickets/${id}`); + }, + + create(data: { + title: string; + description: string; + priority?: TicketPriority; + categoryId?: number; + creatorId: number; + }): Promise { + return request('/tickets', { + method: 'POST', + body: JSON.stringify(data), + }); + }, + + updateStatus(ticketId: number, status: TicketStatus, reason?: string): Promise { + return request(`/tickets/${ticketId}/status`, { + method: 'PATCH', + body: JSON.stringify({ status, reason }), + }); + }, + + assign(ticketId: number, assigneeId: number): Promise { + return request(`/tickets/${ticketId}/assign`, { + method: 'PATCH', + body: JSON.stringify({ assigneeId }), + }); + }, + + updatePriority(ticketId: number, priority: TicketPriority): Promise { + return request(`/tickets/${ticketId}/priority`, { + method: 'PATCH', + body: JSON.stringify({ priority }), + }); + }, + + delete(id: number): Promise { + return request(`/tickets/${id}`, { method: 'DELETE' }); + }, + }, + + comments: { + list(ticketId: number): Promise { + return request(`/tickets/${ticketId}/comments`); + }, + + add(ticketId: number, data: { + content: string; + isInternal: boolean; + authorId: number; + }): Promise { + return request(`/tickets/${ticketId}/comments`, { + method: 'POST', + body: JSON.stringify(data), + }); + }, + }, + + users: { + list(): Promise { + return request('/users'); + }, + create(data: { name: string; email: string; password: string; role: 'admin' | 'agent' | 'requester' }): Promise { + return request('/users', { + method: 'POST', + body: JSON.stringify(data), + }); + }, + update(id: number, data: { name?: string; email?: string; password?: string; role?: 'admin' | 'agent' | 'requester'; isActive?: boolean }): Promise { + return request(`/users/${id}`, { + method: 'PATCH', + body: JSON.stringify(data), + }); + }, + delete(id: number): Promise { + return request(`/users/${id}`, { method: 'DELETE' }); + } + }, +}; diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..66e73d2 --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,16 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import '@/styles/index.css'; +import App from './App'; + +const rootElement = document.getElementById('root'); + +if (!rootElement) { + throw new Error('Elemento #root não encontrado no DOM. Verifique o index.html.'); +} + +createRoot(rootElement).render( + + + , +); diff --git a/web/src/pages/DashboardPage.module.css b/web/src/pages/DashboardPage.module.css new file mode 100644 index 0000000..59e1a73 --- /dev/null +++ b/web/src/pages/DashboardPage.module.css @@ -0,0 +1,160 @@ +.page { + display: flex; + flex-direction: column; + gap: var(--space-6); +} + +.pageHeader { + display: flex; + align-items: flex-end; + justify-content: space-between; +} + +.heading { + font-size: var(--text-2xl); + font-weight: 700; + color: var(--color-text-primary); + letter-spacing: -0.03em; +} + +.total { + font-size: var(--text-sm); + color: var(--color-text-muted); + margin-top: var(--space-1); +} + +/* ── KPI Cards ─────────────────────────────────────────────────────────────── */ +.kpiGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--space-4); + margin-bottom: var(--space-2); +} + +.kpiCard { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-1); +} + +.kpiLabel { + font-size: var(--text-xs); + font-weight: 600; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.kpiValue { + font-size: var(--text-3xl); + font-weight: 700; + color: var(--color-text-primary); + line-height: 1; +} + +/* ── Filtros ───────────────────────────────────────────────────────────────── */ +.filters { + display: flex; + align-items: center; + gap: var(--space-3); + flex-wrap: wrap; +} + +/* ── Grid de tickets ───────────────────────────────────────────────────────── */ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: var(--space-4); +} + +/* ── Skeleton ──────────────────────────────────────────────────────────────── */ +.skeleton { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: var(--space-5); + display: flex; + flex-direction: column; + gap: var(--space-3); + overflow: hidden; +} + +.skeletonLine { + border-radius: var(--radius-sm); + background: linear-gradient( + 90deg, + var(--color-surface-2) 25%, + var(--color-border) 50%, + var(--color-surface-2) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} + +@keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +/* ── Estado vazio ──────────────────────────────────────────────────────────── */ +.emptyState { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-3); + padding: var(--space-16) var(--space-8); + text-align: center; +} + +.emptyTitle { + font-size: var(--text-lg); + font-weight: 600; + color: var(--color-text-secondary); +} + +.emptyMessage { + font-size: var(--text-sm); + color: var(--color-text-muted); +} + +/* ── Estado de erro ────────────────────────────────────────────────────────── */ +.errorState { + padding: var(--space-6); + background: rgba(248, 113, 113, 0.08); + border: 1px solid rgba(248, 113, 113, 0.25); + border-radius: var(--radius-lg); + text-align: center; +} + +.errorTitle { + font-weight: 600; + color: var(--color-priority-critical); + margin-bottom: var(--space-1); +} + +.errorMessage { + font-size: var(--text-sm); + color: var(--color-text-secondary); +} + +/* ── Paginação ─────────────────────────────────────────────────────────────── */ +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-4); + padding-top: var(--space-4); + border-top: 1px solid var(--color-border); +} + +.pageInfo { + font-size: var(--text-sm); + color: var(--color-text-secondary); + min-width: 120px; + text-align: center; +} diff --git a/web/src/pages/DashboardPage.tsx b/web/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..fa7c5b5 --- /dev/null +++ b/web/src/pages/DashboardPage.tsx @@ -0,0 +1,224 @@ +/** + * DashboardPage.tsx + * + * Painel de fila de tickets — visão principal do agente de TI. + * Exibe todos os chamados com filtros por status e prioridade. + */ + +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { api, type TicketStatus, type TicketPriority } from '@/lib/api'; +import { TicketCard } from '@/components/tickets/TicketCard'; +import styles from './DashboardPage.module.css'; + +// ─── Opções dos filtros ─────────────────────────────────────────────────────── + +const STATUS_OPTIONS: { value: TicketStatus | ''; label: string }[] = [ + { value: '', label: 'Todos os status' }, + { value: 'open', label: '🔵 Abertos' }, + { value: 'in_progress', label: '🟣 Em Andamento' }, + { value: 'waiting', label: '🟡 Aguardando' }, + { value: 'resolved', label: '🟢 Resolvidos' }, + { value: 'closed', label: '⚫ Fechados' }, +]; + +const PRIORITY_OPTIONS: { value: TicketPriority | ''; label: string }[] = [ + { value: '', label: 'Todas as prioridades' }, + { value: 'critical', label: '⚡ Crítica' }, + { value: 'high', label: '↑ Alta' }, + { value: 'medium', label: '→ Média' }, + { value: 'low', label: '↓ Baixa' }, +]; + +// ─── Skeleton de carregamento ───────────────────────────────────────────────── + +function TicketSkeleton() { + return ( +