diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c1f11a3c..efd66665 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,5 +9,5 @@ Summary of the change, and the issue it fixes if any (e.g. `Closes #123`). - [ ] Targets `dev`; one concern per PR - [ ] `pnpm check` is green locally (tests + build) - [ ] Logic changes ship with a spec in `tests/` -- [ ] User-visible copy lands in all five `full` locales (`en` / `zh` / `zh-TW` / `fr` / `ru`) +- [ ] User-visible copy lands in every `full` locale (`en` / `zh` / `zh-TW` / `fr` / `ru` / `pt-BR`) - [ ] I've read [CONTRIBUTING.md](../CONTRIBUTING.md) and the relevant `AGENTS.md` diff --git a/.gitignore b/.gitignore index 228e446e..3c6db2ed 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,7 @@ localtest .VSCodeCounter -# Offline datasets (common/datasets.js): every common/-db/ directory +# Offline datasets (server/datasets/datasets.js): every common/-db/ directory # holds only downloaded data, its state and lock — created at runtime, never # committed (CAIDA's data is under a non-commercial license). common/*-db/ diff --git a/AGENTS.md b/AGENTS.md index 42dea25b..ec867db5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,7 +33,7 @@ Express 5 back-end API. | Build | Vite + `@vitejs/plugin-vue`; Tailwind CSS v4 + `tw-animate-css` | | UI | shadcn-vue copy-in primitives (reka-ui) · lucide icons · circle-flags via `@iconify/vue` · vaul-vue drawer · vue-sonner toast | | Backend | Express 5 | -| Logger | `pino` singleton at `common/logger.js` (+ `pino-http`, opt-in) | +| Logger | `pino` singleton at `server/logger.js` (+ `pino-http`, opt-in) | | Auth | Firebase Auth (optional, env-gated) | | Error monitoring | Sentry — optional & env-gated on both halves: `@sentry/vue` (no `VITE_SENTRY_DSN_FRONTEND`, no Sentry in the build — see frontend/AGENTS.md) + `@sentry/node` (no `SENTRY_DSN_BACKEND`, never loaded — see api/AGENTS.md) | | PWA | `manifest.webmanifest` only — installable but online-only, no service worker | @@ -62,8 +62,11 @@ use npm / yarn — they'd produce a competing lockfile. ├── AGENTS.md / CLAUDE.md ← this file + Claude pointer to it ├── frontend/ ← Vue 3 SPA (see frontend/AGENTS.md) ├── api/ ← Express handlers (see api/AGENTS.md) -├── common/ ← code shared by both halves (valid-ip / -│ fetch-with-timeout / guards / logger / …) +├── server/ ← backend-only code (logger / guards / …; +│ datasets/ = the offline dataset engine) +├── common/ ← code the frontend or build imports too +│ (valid-ip / fetch-with-timeout / …) + the +│ dataset dirs (`*-db/`, a self-host mount point) ├── tests/ ← Node test runner specs ├── backend-server.js ← Express app (default port 11966) ├── sentry-instrument.js ← backend Sentry bootstrap via `node --import`; @@ -112,7 +115,7 @@ use npm / yarn — they'd produce a competing lockfile. ### Logging (backend) -- **Always the shared logger** (`common/logger.js`) in backend files; bare +- **Always the shared logger** (`server/logger.js`) in backend files; bare `console.*` is banned there (frontend keeps using `console.*`). - Pino first-arg-is-context: `logger.error({ err, ip }, 'short message')`. - Env knobs: `LOG_LEVEL` (default info), `LOG_FORMAT=json` for shippers, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b65cf2f4..c3fa7193 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,7 +36,8 @@ from building something that can't be merged. ``` frontend/ Vue 3 SPA (Pinia, vue-router, vue-i18n, Tailwind v4 + shadcn-vue) api/ Express 5 handlers, one file per route (wired in backend-server.js) -common/ Code shared by both halves (validators, fetch helper, logger, …) +server/ Backend-only code (logger, guards, upstream clients, dataset engine, …) +common/ Code both halves import (validators, fetch helper, report schema, …) tests/ Node test runner specs (node --test) ``` @@ -58,10 +59,10 @@ Details and rationale live in the AGENTS.md files; the headlines: - **New functions use `const` arrow syntax** (`const fn = async () => {}`). - **Every new file opens with a header comment** stating its purpose. - **The `full` locales land together** — user-visible copy ships in `en` / `zh` / `zh-TW` / - `fr` / `ru` in the same PR (`frontend/locales/`; tests enforce it). `beta` locales are - exempt — they fall back to English. -- **Backend logging goes through the shared pino logger** (`common/logger.js`) — - no `console.*` in `api/` or `common/`. + `fr` / `ru` / `pt-BR` in the same PR (`frontend/locales/`; tests enforce it). `beta` + locales are exempt — they fall back to English. +- **Backend logging goes through the shared pino logger** (`server/logger.js`) — + no `console.*` in `api/`, `server/` or `common/`. ## Good places to start @@ -80,19 +81,22 @@ runnable directly as `pnpm fetch-favicons`). Only if auto-fetch can't find a usa PNG do you hand-source one (on macOS, `sips` handles ICO→PNG). Remember to commit the PNGs with your change — CI stays offline and only checks they exist. -**README translations** — beyond the five READMEs we maintain (`en` / `zh` / `zh-TW` / `fr` / -`ru`), community-maintained translations in any language are welcome. Create +**README translations** — beyond the READMEs we maintain (`en` / `zh` / `zh-TW` / `fr` / +`ru`) and the community-maintained `pt-BR` one, translations in any language are welcome. Create `README_.md` from the English [README.md](README.md) (the canonical source), open it with a one-line "community-maintained translation — English is canonical" note in your language, and add your language to the switcher row near the top of the existing READMEs. Keep code blocks, URLs, and badges unchanged. Check open issues for requested languages before starting. -**UI translations** — improvements to the existing `en` / `zh` / `zh-TW` / `fr` / `ru` packs -are welcome, and so is a whole new language. Adding one is two files: a locale pack under -`frontend/locales/` and a line in `common/locale-registry.js`. **A partial pack is a -welcome first PR** — anything you don't translate falls back to English, and the language -ships as `beta` until it's complete. Full walkthrough: **[TRANSLATING.md](TRANSLATING.md)**. +**UI translations** — improvements to the existing `en` / `zh` / `zh-TW` / `fr` / `ru` / +`pt-BR` packs are always welcome. New languages are added by visitor demand, not on request: +the open **Translations wanted** issue lists the ones currently up for claiming, and a pack +for any other language won't be merged. For a listed language, adding it is two files: a +locale pack under `frontend/locales/` and a line in `common/locale-registry.js`. **A partial +pack is a welcome first PR** — anything you don't translate falls back to English, and the +language ships as `beta` until it's complete. Full walkthrough: +**[TRANSLATING.md](TRANSLATING.md)**. ## Bugs & feature requests diff --git a/Dockerfile b/Dockerfile index 64aa8358..e62d683a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,7 @@ COPY --from=build-stage /app/frontend-server.js ./ # npm start boots the backend with `--import ./sentry-instrument.js` COPY --from=build-stage /app/sentry-instrument.js ./ COPY --from=build-stage /app/api ./api +COPY --from=build-stage /app/server ./server COPY --from=build-stage /app/common ./common EXPOSE 18966 diff --git a/README.md b/README.md index 858d9c6d..50c1c6b9 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ ![Docker Build and Push](https://github.com/jason5ng32/MyIP/actions/workflows/docker-image.yml/badge.svg?branch=main) [![CI](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml) -[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md) +[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md) | [Shqip](README_SQ.md) An open-source, all-in-one IP toolbox: IP lookup from multiple sources, connectivity tests, WebRTC & DNS-leak detection, speed test, MTR, censorship checks, Whois, and more — self-hostable with one Docker command. @@ -44,6 +44,7 @@ Feel free to bookmark the demo or deploy your own. * 🚥 **WebRTC Detection**: Reveals the IP address exposed during WebRTC connections — including whether your browser's privacy hardening is on. * 🛑 **DNS Leak Test**: Shows which DNS endpoints resolve your queries, to evaluate the risk of DNS leaks when using VPNs or proxies. +* 🛡️ **Ad & Tracker Blocking Test**: Checks what your ad blocker, browser, DNS filter, VPN or router actually blocks — ad networks, analytics, social trackers, error monitoring, device-maker telemetry, tracking scripts and element hiding. * 📋 **Security Checklist**: A 258-item personal cybersecurity checklist across 12 areas, with progress saved in your browser. ### 📡 Network Tests @@ -167,6 +168,10 @@ DOMAIN,ptest-5.ipcheck.ing,Proxy5 DOMAIN,ptest-6.ipcheck.ing,Proxy6 DOMAIN,ptest-7.ipcheck.ing,Proxy7 DOMAIN,ptest-8.ipcheck.ing,Proxy8 +DOMAIN,ptest-9.ipcheck.ing,Proxy9 +DOMAIN,ptest-10.ipcheck.ing,Proxy10 +DOMAIN,ptest-11.ipcheck.ing,Proxy11 +DOMAIN,ptest-12.ipcheck.ing,Proxy12 ``` diff --git a/README_FR.md b/README_FR.md index dfc748e7..85727c86 100644 --- a/README_FR.md +++ b/README_FR.md @@ -22,7 +22,7 @@ ![Docker Build and Push](https://github.com/jason5ng32/MyIP/actions/workflows/docker-image.yml/badge.svg?branch=main) [![CI](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml) -[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md) +[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md) | [Shqip](README_SQ.md) Une boîte à outils IP tout-en-un et open source : recherche d'IP depuis plusieurs sources, tests de connectivité, détection WebRTC et de fuites DNS, test de vitesse, MTR, vérification de la censure, Whois et plus encore — auto-hébergeable en une seule commande Docker. @@ -47,6 +47,7 @@ N'hésitez pas à mettre la démo en favori ou à déployer votre propre instanc * 🚥 **Détection WebRTC** : Révèle l'adresse IP exposée lors des connexions WebRTC — y compris si les protections de confidentialité de votre navigateur sont actives. * 🛑 **Test de fuite DNS** : Montre quels points de terminaison DNS résolvent vos requêtes, afin d'évaluer le risque de fuite DNS lors de l'utilisation d'un VPN ou d'un proxy. +* 🛡️ **Test de blocage des pubs et traceurs** : Vérifie ce que votre bloqueur de pubs, navigateur, filtre DNS, VPN ou routeur bloque vraiment — régies publicitaires, statistiques, traceurs sociaux, suivi des erreurs, télémétrie des fabricants d'appareils, scripts de pistage et masquage d'éléments. * 📋 **Liste de contrôle de cybersécurité** : Une liste personnelle de 258 points de cybersécurité répartis en 12 domaines, avec progression sauvegardée dans votre navigateur. ### 📡 Tests réseau @@ -170,6 +171,10 @@ DOMAIN,ptest-5.ipcheck.ing,Proxy5 DOMAIN,ptest-6.ipcheck.ing,Proxy6 DOMAIN,ptest-7.ipcheck.ing,Proxy7 DOMAIN,ptest-8.ipcheck.ing,Proxy8 +DOMAIN,ptest-9.ipcheck.ing,Proxy9 +DOMAIN,ptest-10.ipcheck.ing,Proxy10 +DOMAIN,ptest-11.ipcheck.ing,Proxy11 +DOMAIN,ptest-12.ipcheck.ing,Proxy12 ``` diff --git a/README_PT-BR.md b/README_PT-BR.md index ac96c53a..b3d2fb9d 100644 --- a/README_PT-BR.md +++ b/README_PT-BR.md @@ -22,7 +22,7 @@ ![Docker Build and Push](https://github.com/jason5ng32/MyIP/actions/workflows/docker-image.yml/badge.svg?branch=main) [![CI](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml) -[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md) +[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md) | [Shqip](README_SQ.md) Uma caixa de ferramentas de IP completa e de código aberto: consulta de IP a partir de múltiplas fontes, testes de conectividade, detecção de WebRTC e de vazamento de DNS, teste de velocidade, MTR, verificação de censura, Whois e muito mais — auto-hospedável com um único comando Docker. @@ -47,6 +47,7 @@ Adicione a demonstração aos favoritos ou faça sua própria implantação. * 🚥 **Detecção de WebRTC**: Revela o endereço IP exposto durante conexões WebRTC — inclusive se as proteções de privacidade do seu navegador estão ativas. * 🛑 **Teste de vazamento de DNS**: Mostra quais endpoints DNS resolvem suas consultas, para avaliar o risco de vazamentos de DNS ao usar VPNs ou proxies. +* 🛡️ **Teste de bloqueio de anúncios e rastreadores**: Verifica o que seu bloqueador de anúncios, navegador, filtro de DNS, VPN ou roteador realmente bloqueia — redes de anúncios, análise, rastreadores sociais, monitoramento de erros, telemetria de fabricantes de dispositivos, scripts de rastreamento e ocultação de elementos. * 📋 **Checklist de segurança**: Um checklist pessoal de cibersegurança com 258 itens em 12 áreas, com progresso salvo no seu navegador. ### 📡 Testes de rede @@ -170,6 +171,10 @@ DOMAIN,ptest-5.ipcheck.ing,Proxy5 DOMAIN,ptest-6.ipcheck.ing,Proxy6 DOMAIN,ptest-7.ipcheck.ing,Proxy7 DOMAIN,ptest-8.ipcheck.ing,Proxy8 +DOMAIN,ptest-9.ipcheck.ing,Proxy9 +DOMAIN,ptest-10.ipcheck.ing,Proxy10 +DOMAIN,ptest-11.ipcheck.ing,Proxy11 +DOMAIN,ptest-12.ipcheck.ing,Proxy12 ``` diff --git a/README_RU.md b/README_RU.md index 6b7e7056..faca4274 100644 --- a/README_RU.md +++ b/README_RU.md @@ -22,7 +22,7 @@ ![Docker Build and Push](https://github.com/jason5ng32/MyIP/actions/workflows/docker-image.yml/badge.svg?branch=main) [![CI](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml) -[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md) +[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md) | [Shqip](README_SQ.md) Универсальный набор IP-инструментов с открытым исходным кодом: определение IP по нескольким источникам, проверка доступности сайтов, обнаружение утечек WebRTC и DNS, тест скорости, MTR, проверка цензуры, Whois и многое другое — разворачивается на своём сервере одной командой Docker. @@ -47,6 +47,7 @@ * 🚥 **Обнаружение WebRTC**: показывает IP-адрес, раскрываемый при подключениях WebRTC, — в том числе включена ли в вашем браузере усиленная защита приватности. * 🛑 **Тест утечки DNS**: показывает, какие конечные точки DNS обрабатывают ваши запросы, чтобы оценить риск утечки DNS при использовании VPN или прокси. +* 🛡️ **Проверка блокировки рекламы и трекеров**: показывает, что на самом деле блокирует ваш блокировщик рекламы, браузер, DNS-фильтр, VPN или роутер, — рекламные сети, аналитику, трекеры соцсетей, мониторинг ошибок, телеметрию производителей устройств, скрипты отслеживания и скрытие элементов. * 📋 **Контрольный список безопасности**: 258 рекомендаций по личной кибербезопасности в 12 разделах; прогресс сохраняется в браузере. ### 📡 Сетевые тесты @@ -170,6 +171,10 @@ DOMAIN,ptest-5.ipcheck.ing,Proxy5 DOMAIN,ptest-6.ipcheck.ing,Proxy6 DOMAIN,ptest-7.ipcheck.ing,Proxy7 DOMAIN,ptest-8.ipcheck.ing,Proxy8 +DOMAIN,ptest-9.ipcheck.ing,Proxy9 +DOMAIN,ptest-10.ipcheck.ing,Proxy10 +DOMAIN,ptest-11.ipcheck.ing,Proxy11 +DOMAIN,ptest-12.ipcheck.ing,Proxy12 ``` diff --git a/README_SQ.md b/README_SQ.md new file mode 100644 index 00000000..2b12a962 --- /dev/null +++ b/README_SQ.md @@ -0,0 +1,216 @@ +# 🧰 MyIP - Një kuti më e mirë mjetesh IP + +> [!NOTE] +> Ky është një përkthim i mirëmbajtur nga komuniteti; README-ja në anglisht është burimi zyrtar dhe ky version mund të mbetet prapa. + +
+ +![IPCheck.ing Banner](https://raw.githubusercontent.com/jason5ng32/MyIP/main/public/github/gh_banner.png) + +jason5ng32%2FMyIP | Trendshift + +[![Mentioned in Awesome Self Hosted](https://awesome.re/mentioned-badge.svg)](https://github.com/awesome-selfhosted/awesome-selfhosted) + +![GitHub Repo stars](https://img.shields.io/github/stars/jason5ng32/MyIP) +![GitHub forks](https://img.shields.io/github/forks/jason5ng32/myip) +![Docker Pulls](https://img.shields.io/docker/pulls/jason5ng32/myip) + +[![Website](https://img.shields.io/website?url=https%3A%2F%2Fipcheck.ing&up_message=online&label=IPCheck.ing 'IPCheck.ing')](https://ipcheck.ing) +![PWA](https://img.shields.io/badge/PWA-Supported-blue) + +![CodeQL](https://github.com/jason5ng32/MyIP/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main) +![Docker Build and Push](https://github.com/jason5ng32/MyIP/actions/workflows/docker-image.yml/badge.svg?branch=main) +[![CI](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml) + +[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md) | [Shqip](README_SQ.md) + +Një kuti mjetesh IP gjithëpërfshirëse dhe me burim të hapur: kërkim IP nga shumë burime, teste lidhshmërie, zbulim i rrjedhjeve WebRTC dhe DNS, test shpejtësie, MTR, kontrolle censure, Whois e shumë të tjera — e vetë-strehueshme me një komandë të vetme Docker. + +👉 Demo: [https://ipcheck.ing](https://ipcheck.ing) + +Mund ta ruani demon te faqeshënuesit ose të vendosni instancën tuaj. + +[![Deploy with Docker](https://raw.githubusercontent.com/jason5ng32/MyIP/main/public/github/Docker.svg)](https://hub.docker.com/r/jason5ng32/myip) + +
+ +## 👀 Veçoritë + +### 🪪 IP-ja dhe identiteti juaj + +* 🛜 **Kartat e IP-së**: Zbulojnë IPv4 dhe IPv6 tuaj nga disa burime të pavarura krah për krah — shtetin, rajonin, qytetin, ASN-në, organizatën dhe zonën kohore lokale të IP-së. +* 🔍 **Kërkimi i IP-së**: Kërkon të njëjtin informacion të detajuar për çdo adresë IP për të cilën jeni kureshtarë. +* 🧾 **Historiku i IP-ve**: Mban një regjistër lokal të IP-ve me të cilat jeni parë, të filtrueshëm sipas llojit dhe shtetit — i ruajtur vetëm në shfletuesin tuaj. +* 🖥️ **Gjurma e shfletuesit**: Llogarit gjurmën e shfletuesit tuaj në disa mënyra dhe tregon çfarë ju bën të identifikueshëm. + +### 🕵️ Rrjedhjet dhe privatësia + +* 🚥 **Zbulimi WebRTC**: Zbulon adresën IP që ekspozohet gjatë lidhjeve WebRTC — përfshirë nëse mbrojtja e privatësisë së shfletuesit tuaj është aktive. +* 🛑 **Testi i rrjedhjes DNS**: Tregon cilat pika fundore DNS i zgjidhin kërkesat tuaja, për të vlerësuar rrezikun e rrjedhjeve DNS kur përdorni VPN ose proxy. +* 🛡️ **Testi i bllokimit të reklamave dhe gjurmuesve**: Kontrollon çfarë bllokojnë vërtet bllokuesi i reklamave, shfletuesi, filtri DNS, VPN-ja ose router-i juaj — rrjetet e reklamave, analitikën, gjurmuesit socialë, monitorimin e gabimeve, telemetrinë e prodhuesve të pajisjeve, skriptet e gjurmimit dhe fshehjen e elementeve. +* 📋 **Lista e sigurisë**: Një listë personale kontrolli për sigurinë kibernetike me 258 pika në 12 fusha, me progresin të ruajtur në shfletuesin tuaj. + +### 📡 Testet e rrjetit + +* 🚦 **Kontrolli i lidhshmërisë**: Teston arritshmërinë e deri në 60 faqeve sipas zgjedhjes suaj, me rezultate të vonesës minimale në disa raunde — plus lista të kuruara për import, nga paketat për shtete te AI, rrjetet sociale, transmetimi, lojërat, zhvilluesit e të tjera. Bazuar në rezultatet, tregon nëse aksesi në internetin global është aktualisht i mundur për ju. +* 🚀 **Testi i shpejtësisë**: Mat shkarkimin, ngarkimin dhe vonesën tuaj kundrejt rrjeteve edge. +* ⏱️ **Testi global i vonesës**: Bën ping objektivit tuaj nga sonda në mbarë botën — zgjidhni shtetet nga të gjitha sondat e disponueshme të Globalping, të grupuara sipas kontinentit. +* 🚉 **Testi MTR**: Ekzekuton MTR nga sonda të shpërndara globalisht për të parë rrugën që ndjekin realisht paketat. +* 🚧 **Kontrolli i censurës**: Tregon ku është bllokuar një faqe interneti në mbarë botën — dhe me çfarë mjetesh. +* 🚏 **Testi i rregullave të proxy-t**: Verifikon që konfigurimi i rregullave të softuerit tuaj proxy funksionon ashtu siç e keni menduar. + +### 🔦 Kërkimi dhe infrastruktura + +* 📟 **Zgjidhësi DNS**: Zgjidh një domen përmes disa zgjidhësve njëkohësisht, të grupuar sipas shtetit — një mënyrë e thjeshtë për të vënë re rrëmbimin ose ndotjen. +* 📓 **Kërkimi Whois**: Kryen kërkime Whois për emra domenesh, adresa IP dhe numra AS. +* 🗄️ **Kërkimi MAC**: Identifikon prodhuesin dhe detajet pas një adrese fizike. +* 🧮 **Kalkulatori IP**: Llogaritje subnetesh, konvertime shënimesh dhe detaje të ndërfaqes IPv6 për çdo IP, prefiks, varg ose listë, të llogaritura lokalisht. +* 🛰️ **Informacioni ASN dhe topologjia drejt furnizuesve**: Direkt në çdo kartë IP — detajet e AS-së, shpalljet historike për prefiksin e IP-së dhe shtigjet nga ASN-ja e saj drejt shtyllës kurrizore Tier 1. +* 🛂 **Profili ASN**: Tabloja e plotë e çdo AS — regjistrimi me të dhënat e papërpunuara Whois, renditja dhe madhësia globale, trafiku dhe cilësia e lidhjes, ku përdoret hapësira e saj IPv4, çdo prefiks i shpallur me gjendjen RPKI, furnizuesit, peer-at dhe klientët e saj, si dhe pikat e shkëmbimit të internetit dhe qendrat e të dhënave ku është e pranishme. +* 📶 **Gjendja e shërbimeve**: Disponueshmëria live e shërbimeve të njohura — Claude, OpenAI, GitHub, Cloudflare e të tjera — nga faqet e tyre zyrtare të gjendjes, me incidentet e fundit. + +### ✨ Platforma + +* 📤 **Raporte për t'u ndarë**: Ktheni rezultatet e testeve tuaja në një raport diagnostikues — një lidhje vetëm për lexim me skadim automatik, Markdown gati për AI ose JSON. +* ⌨️ **API Curl**: Merrni IP-në tuaj nga terminali me një komandë të vetme `curl`. +* 🌍 **Toka Online**: Një panel që transmeton ngjarjet globale të ndërprerjeve të internetit në kohën kur ndodhin. +* 🌗 **Modaliteti i errët**: Ndjek automatikisht sistemin tuaj, me mundësi ndërrimi manual. +* 📲 **PWA**: Mund të instalohet si aplikacion në telefon dhe si aplikacion Chrome në desktop. +* ⚡ **Shkurtoret e tastierës**: Çdo funksion ka një të tillë — shtypni `?` për të parë listën. +* 🔤 **Shumë gjuhë**: Ndërfaqja vjen në 6 gjuhë, dhe shtimi i gjuhës suaj kërkon vetëm një paketë lokalizimi. + +## 📕 Si ta përdorni + +### Me Docker + +Një komandë dhe jeni gati: + +```bash +docker run -d -p 18966:18966 --name myip --restart always jason5ng32/myip:latest +``` + +Ose klikoni butonin "Deploy with Docker" në krye të kësaj faqeje. + +### Vendosja në një mjedis Node + +Sigurohuni që keni të instaluar Node.js, pastaj klononi kodin: + +```bash +git clone https://github.com/jason5ng32/MyIP.git +``` + +Instaloni dhe ndërtoni. Ky projekt përdor pnpm — nëse nuk e keni, instalojeni së pari (npm vjen me Node, ndaj kjo komandë funksionon gjithmonë): + +```bash +npm install -g pnpm +pnpm install && pnpm run build +``` + +Ekzekutoni: + +```bash +pnpm start +``` + +Programi do të ekzekutohet në portën 18966. + +## ⚙️ Konfigurimi + +> [!IMPORTANT] +> **Kredencialet e MaxMind GeoLite2 janë të detyrueshme.** Ato mundësojnë gjeolokacionin e IP-ve dhe kërkimet ASN — pa to, burimi MaxMind kthen 503. Janë falas: → [MaxMind Setup](https://docs.ipcheck.ing/developer/getting-started/maxmind-setup) + +> [!WARNING] +> **`ALLOWED_DOMAINS` është i detyrueshëm në një domen real.** Është lista e lejuar e emrave të hosteve për API-në e backend-it — pa të, çdo kërkesë nga një domen që nuk është localhost merr 403. → [Reverse Proxy & Domains](https://docs.ipcheck.ing/developer/getting-started/reverse-proxy-and-domains) + +```bash +docker run -d -p 18966:18966 \ + -e MAXMIND_ACCOUNT_ID="YOUR_ACCOUNT_ID" \ + -e MAXMIND_LICENSE_KEY="YOUR_LICENSE_KEY" \ + -e DATASET_AUTO_UPDATE="true" \ + -e ALLOWED_DOMAINS="your-domain.com" \ + --name myip --restart always \ + jason5ng32/myip:latest +``` + +Gjithçka tjetër — çelësat opsionalë API, siguria dhe kufizimi i shpejtësisë, regjistrimi, Sentry, domenet e API-së curl — është dokumentuar te [Environment Variables reference](https://docs.ipcheck.ing/developer/reference/environment-variables). + +## 📖 Dokumentacioni + +Udhëzuesit e plotë ndodhen në Qendrën e Dokumentacionit të MyIP: **[docs.ipcheck.ing](https://docs.ipcheck.ing)** + +* [Developer Guide](https://docs.ipcheck.ing/developer) — vendosja, konfigurimi, arkitektura dhe kontributi +* [Knowledge Base](https://docs.ipcheck.ing/knowledge-base) — si të përdorni çdo mjet, diagnostikimi i rrjetit hap pas hapi dhe konceptet e rrjetit + +## 🤝 Kontributi + +Kontributet janë të mirëpritura! Mbajmë një grup të kuruar detyrash të përshtatshme për fillestarët — secila me shtigje të sakta skedarësh, kritere pranimi dhe teste që ju udhëheqin drejt një ndërtimi të suksesshëm: + +* 🏷️ [Good first issues](https://github.com/jason5ng32/MyIP/labels/good%20first%20issue) — shtoni një zgjidhës DNS nga shteti juaj, shtoni lista të kuruara faqesh, përktheni README-në në gjuhën tuaj, përmirësoni përkthimet e të tjera +* 🌐 [TRANSLATING.md](TRANSLATING.md) — sillni ndërfaqen në gjuhën tuaj: një paketë lokalizimi plus një rresht në regjistër, dhe **një përkthim i pjesshëm është një PR i parë i mirëpritur** +* 📄 [CONTRIBUTING.md](CONTRIBUTING.md) — konfigurimi, konventat dhe si rrjedhin PR-të (synoni degën `dev`) + +## 👩🏻‍💻 Përdorimi i avancuar + +
+Rregulla proxy për të kontrolluar njëkohësisht IP-në tuaj të vërtetë dhe IP-në e proxy-t + +Nëse përdorni një proxy për t'u lidhur me internetin, mendoni të shtoni këtë rregull në konfigurimin e proxy-t tuaj (përshtateni sipas klientit tuaj). Ky konfigurim ju lejon të kontrolloni si IP-në tuaj të vërtetë, ashtu edhe IP-në kur përdorni proxy-n: + +```ini +# IP Testing +IP-CIDR,1.0.0.2/32,Proxy,no-resolve +IP-CIDR6,2606:4700:4700::1111/128,Proxy,no-resolve +DOMAIN,4.ipcheck.ing,DIRECT +DOMAIN,6.ipcheck.ing,DIRECT +# Rule Testing +DOMAIN,ptest-1.ipcheck.ing,Proxy1 +DOMAIN,ptest-2.ipcheck.ing,Proxy2 +DOMAIN,ptest-3.ipcheck.ing,Proxy3 +DOMAIN,ptest-4.ipcheck.ing,Proxy4 +DOMAIN,ptest-5.ipcheck.ing,Proxy5 +DOMAIN,ptest-6.ipcheck.ing,Proxy6 +DOMAIN,ptest-7.ipcheck.ing,Proxy7 +DOMAIN,ptest-8.ipcheck.ing,Proxy8 +DOMAIN,ptest-9.ipcheck.ing,Proxy9 +DOMAIN,ptest-10.ipcheck.ing,Proxy10 +DOMAIN,ptest-11.ipcheck.ing,Proxy11 +DOMAIN,ptest-12.ipcheck.ing,Proxy12 +``` + +
+ +## 💖 Sponsorët dhe mbështetësit + +Si projekt me burim të hapur, jam shumë mirënjohës për mbështetjen e sponsorëve të mëposhtëm: + +**Sponsorët** + +RapidProxy
+↳ **RapidProxy** — Residential proxies with 90M+ IPs, smart rotation and precise geo-targeting. From $0.55/GB, 10% off with code `RAPID10` + +SwiftProxy
+↳ **SwiftProxy** — High-quality rotating and static residential proxies, with 90M+ residential IPs across 220+ locations worldwide. It supports HTTP(S), SOCKS5, rotating and sticky sessions, with residential proxy plans starting at just $0.7/GB. Use code `PROXY90` for an additional 10% off. + + +Proxy-Seller
+↳ **Proxy-Seller** — offers private residential, mobile (4G/5G/LTE), ISP, and datacenter IPv4/IPv6 proxies with HTTP(S) and SOCKS5 support, 47M+ residential IPs in 220+ locations, precise country/city/ISP targeting, 99.7% uptime, and 24/7 support. MyIP users can use promo code `CHECKINGIP15` to get 15% off. + + +V.PS
+↳ **V.PS** — High-performance KVM VPS with global locations, fast connectivity, and IPv4 & IPv6 included. + +---- + +**Mbështetësit** + +

Cloudflare Project Alexandria

+

1Password

+

Greptile

+

Sentry

+

GitBook

+ +Jeni të interesuar të sponsorizoni MyIP? [Bëhu sponsor](https://github.com/sponsors/jason5ng32) + +## 📄 Licenca + +[MIT](LICENSE) © Jason Ng diff --git a/README_ZH-TW.md b/README_ZH-TW.md index 08f8a2ba..4fcc9e3e 100644 --- a/README_ZH-TW.md +++ b/README_ZH-TW.md @@ -22,7 +22,7 @@ ![Docker Build and Push](https://github.com/jason5ng32/MyIP/actions/workflows/docker-image.yml/badge.svg?branch=main) [![CI](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml) -[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md) +[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md) | [Shqip](README_SQ.md) 一個開源的多合一 IP 工具箱:多來源 IP 查詢、連通性測試、WebRTC 與 DNS 洩漏偵測、網速測試、MTR、審查封鎖檢查、Whois 等——一條 Docker 指令即可自行架設。 @@ -47,6 +47,7 @@ * 🚥 **WebRTC 洩漏測試**:揭示 WebRTC 連線過程中暴露的 IP 位址——包括你的瀏覽器隱私強化設定是否已開啟。 * 🛑 **DNS 洩漏測試**:顯示是哪些 DNS 端點在解析你的查詢,以評估使用 VPN 或代理時 DNS 洩漏的風險。 +* 🛡️ **廣告與追蹤封鎖檢測**:檢查你的廣告阻擋器、瀏覽器、DNS 過濾、VPN 或路由器實際擋下了什麼——涵蓋廣告網路、統計分析、社群平台追蹤、錯誤監控、裝置廠商遙測、追蹤腳本與元素隱藏。 * 📋 **安全檢查清單**:一份涵蓋 12 個領域、共 258 個項目的個人網路安全檢查清單,進度儲存在你的瀏覽器裡。 ### 📡 網路測試 @@ -170,6 +171,10 @@ DOMAIN,ptest-5.ipcheck.ing,Proxy5 DOMAIN,ptest-6.ipcheck.ing,Proxy6 DOMAIN,ptest-7.ipcheck.ing,Proxy7 DOMAIN,ptest-8.ipcheck.ing,Proxy8 +DOMAIN,ptest-9.ipcheck.ing,Proxy9 +DOMAIN,ptest-10.ipcheck.ing,Proxy10 +DOMAIN,ptest-11.ipcheck.ing,Proxy11 +DOMAIN,ptest-12.ipcheck.ing,Proxy12 ``` diff --git a/README_ZH.md b/README_ZH.md index 23e1df4e..9059505a 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -22,7 +22,7 @@ ![Docker Build and Push](https://github.com/jason5ng32/MyIP/actions/workflows/docker-image.yml/badge.svg?branch=main) [![CI](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jason5ng32/MyIP/actions/workflows/ci.yml) -[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md) +[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_ZH-TW.md) | [Русский](README_RU.md) | [Français](README_FR.md) | [Português (BR)](README_PT-BR.md) | [Shqip](README_SQ.md) 一个开源的一站式 IP 工具箱:多来源 IP 查询、连通性测试、WebRTC 与 DNS 泄露检测、网速测试、MTR、封锁检测、Whois 等等 —— 一条 Docker 命令即可自行部署。 @@ -47,6 +47,7 @@ * 🚥 **WebRTC 检测**:揭示 WebRTC 连接过程中暴露的 IP 地址 —— 包括你的浏览器是否开启了隐私强化。 * 🛑 **DNS 泄露测试**:展示是哪些 DNS 出口在解析你的请求,以便评估使用 VPN 或代理时 DNS 泄露的风险。 +* 🛡️ **广告与跟踪屏蔽检测**:检查你的广告拦截器、浏览器、DNS 过滤、VPN 或路由器实际拦住了什么——涵盖广告网络、统计分析、社交平台跟踪、错误监控、设备厂商遥测、跟踪脚本与元素隐藏。 * 📋 **安全检查清单**:一份覆盖 12 个领域、共 258 项的个人网络安全检查清单,进度保存在你的浏览器里。 ### 📡 网络测试 @@ -170,6 +171,10 @@ DOMAIN,ptest-5.ipcheck.ing,Proxy5 DOMAIN,ptest-6.ipcheck.ing,Proxy6 DOMAIN,ptest-7.ipcheck.ing,Proxy7 DOMAIN,ptest-8.ipcheck.ing,Proxy8 +DOMAIN,ptest-9.ipcheck.ing,Proxy9 +DOMAIN,ptest-10.ipcheck.ing,Proxy10 +DOMAIN,ptest-11.ipcheck.ing,Proxy11 +DOMAIN,ptest-12.ipcheck.ing,Proxy12 ``` diff --git a/SECURITY.md b/SECURITY.md index 6cb9aabb..f13b575e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,8 +18,8 @@ Helpful things to include: - What an attacker can do with it, and what they'd need to start (a session? just a URL?). - Steps to reproduce — a request, a payload, or a short script. -- Where the problem lives: front-end (`frontend/`), API handler (`api/`), or shared - code (`common/`). +- Where the problem lives: front-end (`frontend/`), API handler (`api/`), backend + code (`server/`), or shared code (`common/`). - Whether you hit it on [ipcheck.ing](https://ipcheck.ing) or your own deployment, and the version if self-hosted. diff --git a/TRANSLATING.md b/TRANSLATING.md index bfb7ce17..ae7b4a2b 100644 --- a/TRANSLATING.md +++ b/TRANSLATING.md @@ -1,9 +1,9 @@ # Translating MyIP MyIP (demo website: [IPCheck.ing](https://ipcheck.ing)) ships its UI in English, 简体中文, 繁體中文, -Français and Русский. Adding a sixth — or a seventh — is a two-file change, and **a partial -translation is a welcome first PR**. You do not need to translate everything, and you do not -need to know Vue. +Français, Русский and Português (Brasil). Adding a language is a two-file change, and **a +partial translation is a welcome first PR**. You do not need to translate everything, and you +do not need to know Vue. Anything you leave untranslated falls back to English, so a pack with a hundred strings in it is genuinely useful on day one and can grow over several PRs. Untranslated strings stay @@ -13,6 +13,19 @@ in your pack as `""`, which is what keeps a translation PR readable: every diff General contribution rules (fork, branch from `dev`, `pnpm check` before you open the PR) live in [CONTRIBUTING.md](CONTRIBUTING.md). +## Which languages we take + +**New languages are added by visitor demand, not on request.** Every pack has to follow +every UI change — the English pack grew by roughly 45% over two months in 2026, and each +shipped language followed in the same commits — and each language is one more variant of +the docs site to look after. So a language opens only once it has a meaningful share of +IPCheck.ing's visitors. The open **Translations wanted** issue lists the languages +currently up for claiming; a pack for a language that isn't on it won't be merged, however +good it is. Right-to-left languages also need layout work the app doesn't have yet. + +If your language isn't on the list, a README translation is welcome in any language — that +is a single file and costs nothing to keep (see CONTRIBUTING.md). + --- ## The smallest useful PR @@ -41,7 +54,7 @@ can see at a glance which strings you actually wrote. | File | What it is | Required? | |---|---|---| -| `frontend/locales/.json` | The main pack — every string in the app UI (~1,150 keys) | **Yes** — all keys, values may be `""` | +| `frontend/locales/.json` | The main pack — every string in the app UI (~1,650 keys) | **Yes** — all keys, values may be `""` | | `frontend/locales/privacy/.json` | Privacy policy copy (~50 keys) | No — but whole or not at all | | `frontend/locales/security-checklist/.json` | The Cybersecurity Checklist dataset (~1,080 keys, 258 items) | No — but whole or not at all | | `frontend/data/changelog.json` | Release history, one string per language per entry | No — beta languages are exempt | @@ -184,8 +197,8 @@ them would fail the gate on your behalf. Translate the new keys, or drop the fil ## Improving an existing language -Fixes and better phrasings for `en` / `zh` / `zh-TW` / `fr` / `ru` are just as welcome as new -languages — edit the JSON and open the PR. Two things to keep in mind: +Fixes and better phrasings for `en` / `zh` / `zh-TW` / `fr` / `ru` / `pt-BR` are just as +welcome as new languages — edit the JSON and open the PR. Two things to keep in mind: - Prefer natural phrasing over literal translation; this is a networking tool, and the jargon most users know is often the English term. diff --git a/api/AGENTS.md b/api/AGENTS.md index 3334a1c7..9abfeb4f 100644 --- a/api/AGENTS.md +++ b/api/AGENTS.md @@ -1,29 +1,36 @@ # api/AGENTS.md -Conventions for Express 5 handlers under `api/` and shared back-end code -under `common/`. Universal rules live in ../AGENTS.md. +Conventions for Express 5 handlers under `api/` and the back-end code under +`server/` and `common/`. Universal rules live in ../AGENTS.md. ## Overview The Express app lives in `backend-server.js` at the repo root — every route -is wired there and delegated to one handler module under `api/`. `common/` -holds shared back-end code (guards, logger, fetch helper, MaxMind / CAIDA -services, service-status poller), parts of which the frontend also imports -(`valid-ip.js`, `fetch-with-timeout.js`). +is wired there and delegated to one handler module under `api/`. Code the +handlers share is split by who imports it: + +- **`server/`** — backend-only (guards, logger, upstream clients, service-status + poller, …), flat; `server/datasets/` is the offline dataset engine and its + readers (see "Offline datasets"). +- **`common/`** — only what the frontend or the build imports too + (`valid-ip.js`, `fetch-with-timeout.js`, `report-schema.js`, …); it never + imports from `server/`. It also holds the dataset directories (`*-db/`), + which self-hosting docs and Docker volumes mount — they stay put. Roughly one handler file per route: IP-geolocation sources (`ipinfo-io` / `ipapi-com` / `ipapi-is` / `ip2location-io` / `ip-sb` / `ipcheck-ing` / `maxmind`), tool backends (`get-whois` / `dns-resolver` / `mac-checker` / `cf-radar` / `asn-history` / `asn-connectivity` / `asn-profile` / `ooni-blocking` / `globalping-probes` / `service-status` / `google-map` / -`github-stars` / `invisibility-test` / `dns-leak-test` / `persona`), user +`github-stars` / `invisibility-test` / `dns-leak-test` / `persona` / +`ip-blocklist`), user proxies (`get-user-info` / `update-user-achievement`), platform (`configs` / `sentry-tunnel` / `share-report`). Each file's header comment states its route and purpose — read those for specifics. The exception to one-file-per-route is Cloudflare Radar: all Radar data rides `/api/cfradar`, dispatched by `?view=` over `RADAR_VIEWS` in -`common/cf-radar.js`, where each view declares its guards, edge-cache TTL and +`server/cf-radar.js`, where each view declares its guards, edge-cache TTL and fetch function — new Radar data is a view function plus a registry row, never a new route. `api/data/` holds contributor-editable static config consumed by handlers — currently `dns-resolvers.js`, the country-annotated resolver list @@ -31,20 +38,20 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`). ### ASN data -- **`/api/asn-profile`** (`common/asn-profile.js`) runs each ASN source's core +- **`/api/asn-profile`** (`server/asn-profile.js`) runs each ASN source's core under a deadline above the upstream timeouts it wraps (`INNER_TIMEOUTS`); a failed or late one is just absent. Always 200 `{ asn, status, incomplete, …sections }` (`ok` / `empty` / `error` / `disabled`; an upstream fault is `error`, never `empty`; `incomplete` = `ok` but partial); 502 if all fail. - **Relationship counts follow the graph:** `asn-connectivity` neighbour - lists come from local CAIDA (`common/as-relationships.js`), dropping the + lists come from local CAIDA (`server/as-relationships.js`), dropping the providers the graph walk distrusts. The Radar `asn` view (ASN Info) carries no prefix or relationship counts — the ASN Profile has both. - **`/api/whois` `?q=`:** IPs (RDAP, whoiser fallback), domains (whoiser, RDAP fallback), `AS` (RDAP autnum); all carry a `__raw` block. - **Offline datasets** (MaxMind, CAIDA, PeeringDB, IEEE MAC registries) are rows in - `common/datasets.js`, run by one engine, - `common/dataset-updater.js` (lock, atomic state, publish, boot download, + `server/datasets/datasets.js`, run by one engine, + `server/datasets/dataset-updater.js` (lock, atomic state, publish, boot download, daily schedule). A row holds only its own logic — find the newest remote version, fetch / distill it, validate, reload; a new dataset is a new row, never a new updater. Fetched at boot when its reader has nothing usable @@ -56,9 +63,9 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`). reloaded by the engine's watcher. MaxMind only with its credentials (City + ASN publish together); PeeringDB only with a Cloudflare key; its 116 MB dump is stream-distilled (never parsed whole) to a ~6 MB per-ASN index without - contact data (`common/peeringdb-distill.js`). The IEEE row (MAC Lookup) + contact data (`server/datasets/peeringdb-distill.js`). The IEEE row (MAC Lookup) needs no key: five registry CSVs published as downloaded, answered by the - longest covering assignment (`common/oui-db.js`). + longest covering assignment (`server/datasets/oui-db.js`). ## Conventions @@ -67,7 +74,7 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`). - **Every upstream call uses `fetchUpstream`** (`common/fetch-with-timeout.js`, 8s timeout), never a bare `fetch()` / `https.get()` — a hanging provider must time out. It injects a default `User-Agent` `MyIP/v/` - (`common/upstream-ua.js`; some WAFs block undici's `node` UA); caller-supplied + (`server/upstream-ua.js`; some WAFs block undici's `node` UA); caller-supplied ones, incl. the private-API `{ ...req.headers }` pass-through, win. - **Error shape.** `res.status(500).json({ error: error.message })` on upstream failure, `400` on bad input. Terse — the frontend doesn't show them verbatim. @@ -76,8 +83,10 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`). sources match it. `timezone` is the exception (see "Response enrichment"). - **`?lang` is never validated in a handler.** Pass the raw tag through: `lookupMaxMind` normalizes it onto the languages its data carries - (`SUPPORTED_LANGS` in `common/maxmind-service.js`); the private-API proxies - forward it for the upstream's own resolution. No allow-lists. + (`SUPPORTED_LANGS` in `server/datasets/maxmind-service.js`); the private-API proxies + forward it for the upstream's own resolution. No allow-lists; `/api/map`'s + `language` only gets a tag-shape check, as each value is a year-cached + Static Maps call. - **Logging.** Shared logger only, `logger.error({ err, ...ctx }, 'msg')`; no `console.*`, no "received request" lines (`pino-http` covers those). - **Error monitoring (Sentry) is env-gated and invisible to handlers.** @@ -87,14 +96,14 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`). loaded; `SENTRY_ENVIRONMENT=development` skips the init. Handlers never import Sentry: throws and 5xx traces are automatic; caught failures stay on the logger (its hook mirrors warn+ to Sentry Logs, error+ to Issues). Periodic - jobs wrap their tick in `common/sentry-cron.js`; `key` / `token` query params - are redacted (`common/sentry-scrub.js`). + jobs wrap their tick in `server/sentry-cron.js`; `key` / `token` query params + are redacted (`server/sentry-scrub.js`). ## Security & Boundaries ### Guards live in middleware, not handlers -`common/guards.js`, attached in `backend-server.js`; handlers never repeat them: +`server/guards.js`, attached in `backend-server.js`; handlers never repeat them: - `requireReferer` — global on `/api/*` (ALLOWED_DOMAINS + localhost). - `requirePublicIP()` — per-route `?ip=`: well-formed and publicly routable; @@ -120,7 +129,7 @@ New param shape → new guard there, attached in `backend-server.js`. ### Response enrichment lives in middleware too -`withTimeZone()` (`common/ip-timezone.js`), on all seven geo routes, derives +`withTimeZone()` (`server/ip-timezone.js`), on all seven geo routes, derives `timezone` (IANA name) from the `latitude` / `longitude` the handler returned (2xx only, the `cacheable` hook); no handler computes or forwards one. Own coordinates keep the zone consistent with the city beside it; only the name @@ -132,7 +141,7 @@ DST switch. A new geo source adds the middleware. The backend listens before any missing offline dataset or the service-status snapshot is fetched; snapshots already on disk load first, so only a first boot waits on downloads. During that window `requireOfflineData([...probes])` -(`common/offline-data.js`) answers 503 on a route whose data is still missing +(`server/offline-data.js`) answers 503 on a route whose data is still missing — never edge-cached — and is a no-op after boot, when a missing dataset means a failed download served degraded. A route reading local data declares its probes (`isMaxMindReady`, `isAsRelLoaded`, …) in `backend-server.js`. Answers @@ -143,17 +152,28 @@ loading (`isStillLoading`) answers `error`, leaving the others to answer. Handlers proxying our private IPCheck.ing API (`ipcheck-ing`, `invisibility-test`, `update-user-achievement`, `get-user-info`, -`dns-leak-test`, `persona`, and `asn-profile`'s reputation via -`common/asn-reputation.js`) forward the caller's headers upstream (`headers: { +`dns-leak-test`, `persona`, `ip-blocklist`, and `asn-profile`'s reputation via +`server/asn-reputation.js`) forward the caller's headers upstream (`headers: { ...req.headers }`) — the upstream needs caller context (Accept-Language, auth tokens). `persona` strips the framing headers (`host` / `content-length` / …) first, as it re-serializes the body. Do **not** replicate for third-party upstreams; those get only what's explicitly needed. +### Firebase Auth handler proxy + +`frontend-server.js` (production) and `vite.config.js` (dev) reverse-proxy +`/__/auth/*` to `.firebaseapp.com` (`common/firebase-auth-proxy.js`) +when `VITE_FIREBASE_PROJECT_ID` is set, so `VITE_FIREBASE_AUTH_DOMAIN` can be +the deployment's own host and the sign-in handler runs first-party — inside +the installed PWA's webview and Safari's storage partition, where +`getRedirectResult` finds the result. Every OAuth provider must accept +`https:///__/auth/handler` per environment. Not an `/api` route: no +guards, caching or per-request logs. + ## Edge caching Every `/api/*` response defaults to `Cache-Control: no-store`; slowly-changing -public routes opt in via `cacheable(maxAge)` (`common/cache-control.js`) in +public routes opt in via `cacheable(maxAge)` (`server/cache-control.js`) in `backend-server.js`, e.g. `cacheable(24 * 60 * 60)` (TTLs written that way, not raw seconds). `maxAge` may be a `(req) => seconds` resolver (cfradar: its view's TTL; falsy = no-store). `{ cacheIf }` vetoes the full TTL for a 2xx body, diff --git a/api/asn-connectivity.js b/api/asn-connectivity.js index 5affe933..995cfb9d 100644 --- a/api/asn-connectivity.js +++ b/api/asn-connectivity.js @@ -8,21 +8,21 @@ // backbone connectivity is settlement-free peering with the Tier 1 clique, // invisible to a p2c-only walk. // -// Data is fully local (common/as-rel-db.js + common/as-org-db.js), so the +// Data is fully local (server/datasets/as-rel-db.js + server/datasets/as-org-db.js), so the // whole BFS is synchronous. We only hit RIPEstat for as-overview as a // rare fallback when as2org doesn't have an ASN's org name. // // Alongside the graph, `neighbours` lists the origin's direct providers, // peers and customers from the same snapshot, counted by the walk's own -// misinference rule (common/as-relationships.js) so the counts never show an +// misinference rule (server/as-relationships.js) so the counts never show an // upstream the graph drops. List names come from as2org, gaps filled from the // graph's resolved names. -import { resolveAsnOrgName } from '../common/ripestat.js'; -import { providersOf, peersOf, customersOf, customerCountOf, isTier1 } from '../common/as-rel-db.js'; -import { lookupAsOrgName } from '../common/as-org-db.js'; -import { TIER1_PEERING_TRUSTED, TIER1_ADJACENCY_TRUSTED, countedRelationships } from '../common/as-relationships.js'; -import logger from '../common/logger.js'; +import { resolveAsnOrgName } from '../server/ripestat.js'; +import { providersOf, peersOf, customersOf, customerCountOf, isTier1 } from '../server/datasets/as-rel-db.js'; +import { lookupAsOrgName } from '../server/datasets/as-org-db.js'; +import { TIER1_PEERING_TRUSTED, TIER1_ADJACENCY_TRUSTED, countedRelationships } from '../server/as-relationships.js'; +import logger from '../server/logger.js'; // How deep to recurse from the origin. 3 covers regional networks reaching // Tier 1s through 1-2 intermediates; deeper just adds noise at the periphery. @@ -33,10 +33,10 @@ const MAX_DEPTH = 3; const MAX_INTERMEDIATE_BRANCH = 3; // Past either bar (TIER1_PEERING_TRUSTED / TIER1_ADJACENCY_TRUSTED, defined -// in common/as-relationships.js so the neighbour counts use the same ones), a +// in server/as-relationships.js so the neighbour counts use the same ones), a // node's non-Tier-1 "providers" are treated as CAIDA misinference and dropped. -// Two-tier org name resolver lives in common/ripestat.js. No onError hook +// Two-tier org name resolver lives in server/ripestat.js. No onError hook // here — connectivity stays silent on as-overview fallback failures (a node // just keeps name=null); asn-history is the one that warns. const resolveOrgName = (asn) => resolveAsnOrgName(asn); diff --git a/api/asn-history.js b/api/asn-history.js index c3ab8a16..c081e34e 100644 --- a/api/asn-history.js +++ b/api/asn-history.js @@ -13,8 +13,8 @@ import { fetchRoutingHistory, resolveAsnOrgName, -} from '../common/ripestat.js'; -import logger from '../common/logger.js'; +} from '../server/ripestat.js'; +import logger from '../server/logger.js'; const prefixLength = (prefix) => parseInt((prefix || '').split('/')[1], 10); @@ -56,7 +56,7 @@ function summarizeOrigin(entry, minLen) { }; } -// Two-tier resolver lives in common/ripestat.js. Here we pass a warn hook so +// Two-tier resolver lives in server/ripestat.js. Here we pass a warn hook so // a failed as-overview fallback stays observable (asn-connectivity omits it // and stays silent — keep that difference). const resolveOrgName = (asn) => diff --git a/api/asn-profile.js b/api/asn-profile.js index 59d677d9..84617860 100644 --- a/api/asn-profile.js +++ b/api/asn-profile.js @@ -1,23 +1,23 @@ // /api/asn-profile?asn= — everything the ASN Profile page shows, in one -// answer: Radar ASN summary + prefix list (common/cf-radar.js), CAIDA +// answer: Radar ASN summary + prefix list (server/cf-radar.js), CAIDA // topology and neighbours (./asn-connectivity.js), RDAP autnum -// (common/rdap.js), ASRank (common/asrank.js), the local PeeringDB index -// (common/peeringdb-db.js) and, when the private API is configured, -// reputation (common/asn-reputation.js). Composition, deadlines and statuses -// live in common/asn-profile.js. `asn` arrives as a canonical number string +// (server/rdap.js), ASRank (server/asrank.js), the local PeeringDB index +// (server/datasets/peeringdb-db.js) and, when the private API is configured, +// reputation (server/asn-reputation.js). Composition, deadlines and statuses +// live in server/asn-profile.js. `asn` arrives as a canonical number string // (requireValidASN). 200 unless every configured source failed (502). -import { RADAR_VIEWS, hasRadarApiKey, loadAsnSummary, isCompleteRadarAnswer } from '../common/cf-radar.js'; -import { rdapAutnum, isAutnumMissing } from '../common/rdap.js'; -import { queryAsRank } from '../common/asrank.js'; -import { requestAsnReputation } from '../common/asn-reputation.js'; -import { isPeeringdbLoaded, lookupPeeringdb } from '../common/peeringdb-db.js'; -import { isAsRelLoaded } from '../common/as-rel-db.js'; -import { isAsOrgLoaded } from '../common/as-org-db.js'; -import { isStillLoading } from '../common/offline-data.js'; +import { RADAR_VIEWS, hasRadarApiKey, loadAsnSummary, isCompleteRadarAnswer } from '../server/cf-radar.js'; +import { rdapAutnum, isAutnumMissing } from '../server/rdap.js'; +import { queryAsRank } from '../server/asrank.js'; +import { requestAsnReputation } from '../server/asn-reputation.js'; +import { isPeeringdbLoaded, lookupPeeringdb } from '../server/datasets/peeringdb-db.js'; +import { isAsRelLoaded } from '../server/datasets/as-rel-db.js'; +import { isAsOrgLoaded } from '../server/datasets/as-org-db.js'; +import { isStillLoading } from '../server/offline-data.js'; import { SOURCE_TIMEOUTS, buildSectionLoaders, composeAsnProfile, allSourcesFailed, -} from '../common/asn-profile.js'; +} from '../server/asn-profile.js'; import { getAsnConnectivity } from './asn-connectivity.js'; // A section whose local data isn't there fails (status 'error'): left out diff --git a/api/cf-radar.js b/api/cf-radar.js index bcb9eae6..c66f4aca 100644 --- a/api/cf-radar.js +++ b/api/cf-radar.js @@ -1,12 +1,12 @@ // /api/cfradar — single dispatch route for all Cloudflare Radar data. -// `?view=` selects an entry in RADAR_VIEWS (common/cf-radar.js): the view's +// `?view=` selects an entry in RADAR_VIEWS (server/cf-radar.js): the view's // guards validate and normalize its params, its fetch builds the payload, // and the route's cache middleware in backend-server.js reads the view's // per-view TTL. Guards run before the API-key check so param errors stay // 400s on keyless deployments (and tests never reach an upstream call). -import { RADAR_VIEWS, hasRadarApiKey } from '../common/cf-radar.js'; -import logger from '../common/logger.js'; +import { RADAR_VIEWS, hasRadarApiKey } from '../server/cf-radar.js'; +import logger from '../server/logger.js'; // Run a view's guard middlewares outside an Express chain. Each guard either // calls next() or writes its own 4xx response — so "didn't pass" means the diff --git a/api/dns-leak-test.js b/api/dns-leak-test.js index c42df789..90606577 100644 --- a/api/dns-leak-test.js +++ b/api/dns-leak-test.js @@ -9,7 +9,7 @@ // GET /api/dnsleaktest/session/:token import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; const TOKEN_RE = /^[0-9a-f]{32}$/; diff --git a/api/dns-resolver.js b/api/dns-resolver.js index 3eadb93b..3baf0b54 100644 --- a/api/dns-resolver.js +++ b/api/dns-resolver.js @@ -3,7 +3,7 @@ // one flat, country-annotated result list the frontend groups by country. import { Resolver } from 'node:dns/promises'; import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; import { DNS_RESOLVERS } from './data/dns-resolvers.js'; import { NAME_VALUED_TYPES } from '../common/dns-record-types.js'; diff --git a/api/get-user-info.js b/api/get-user-info.js index e158cd08..0977ba87 100644 --- a/api/get-user-info.js +++ b/api/get-user-info.js @@ -1,5 +1,5 @@ import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; export default async (req, res) => { const key = process.env.IPCHECKING_API_KEY; diff --git a/api/get-whois.js b/api/get-whois.js index a53312f4..2c5f2ae9 100644 --- a/api/get-whois.js +++ b/api/get-whois.js @@ -4,8 +4,8 @@ import whoiser from 'whoiser'; import { isValidIP, isUsablePublicIP } from '../common/valid-ip.js'; -import { rdapDomain, rdapIp, rdapAutnum, isAutnumMissing } from '../common/rdap.js'; -import logger from '../common/logger.js'; +import { rdapDomain, rdapIp, rdapAutnum, isAutnumMissing } from '../server/rdap.js'; +import logger from '../server/logger.js'; function isValidDomain(domain) { const domainPattern = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i; diff --git a/api/github-stars.js b/api/github-stars.js index 4a2e5aca..6100534d 100644 --- a/api/github-stars.js +++ b/api/github-stars.js @@ -4,7 +4,7 @@ // Cloudflare the origin hits GitHub at most once per cache window — well under // the 60 req/hour unauthenticated limit. import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; // The project's own repository. GitHub requires a User-Agent on every // request; fetchUpstream's default project UA satisfies that. diff --git a/api/globalping-probes.js b/api/globalping-probes.js index 5968bb25..f001d7de 100644 --- a/api/globalping-probes.js +++ b/api/globalping-probes.js @@ -5,8 +5,8 @@ // bytes, probe-country coverage changes slowly. import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import { buildProbeInventory } from '../common/globalping-inventory.js'; -import logger from '../common/logger.js'; +import { buildProbeInventory } from '../server/globalping-inventory.js'; +import logger from '../server/logger.js'; const GLOBALPING_PROBES_URL = 'https://api.globalping.io/v1/probes'; diff --git a/api/google-map.js b/api/google-map.js index fd458aad..b98115a1 100644 --- a/api/google-map.js +++ b/api/google-map.js @@ -1,13 +1,18 @@ import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; + +// Validate request legitimacy. `language` is the UI locale code, region +// subtag included (`pt-BR`, `zh-TW`); like every `?lang` it is not +// allow-listed, but it must be tag-shaped: every distinct value is an +// uncached Static Maps call whose answer is edge-cached for a year. +const LANGUAGE_TAG = /^[a-z]{2,3}(-[A-Za-z0-9]{2,4}){0,2}$/; -// Validate request legitimacy function isValidRequest(req) { const isLatitudeValid = /^-?\d+(\.\d+)?$/.test(req.query.latitude); const isLongitudeValid = /^-?\d+(\.\d+)?$/.test(req.query.longitude); - const isLanguageValid = /^[a-z]{2}$/.test(req.query.language); + const isLanguageValid = typeof req.query.language === 'string' && LANGUAGE_TAG.test(req.query.language); if (!isLatitudeValid || !isLongitudeValid || !isLanguageValid) { return false; @@ -69,7 +74,7 @@ export default async (req, res) => { styleParam = styles.Dark.join('&style='); } - const url = `https://maps.googleapis.com/maps/api/staticmap?center=${latitude},${longitude}&markers=color:blue%7C${latitude},${longitude}&scale=${scale}&zoom=${zoom}&maptype=roadmap&language=${language}&format=${fmt}&size=${mapSize}&style=${styleParam}&key=${apiKey}`; + const url = `https://maps.googleapis.com/maps/api/staticmap?center=${latitude},${longitude}&markers=color:blue%7C${latitude},${longitude}&scale=${scale}&zoom=${zoom}&maptype=roadmap&language=${encodeURIComponent(language)}&format=${fmt}&size=${mapSize}&style=${styleParam}&key=${apiKey}`; // Unlike the JSON handlers, the Static Maps response is a binary JPEG // and must be passed through to the client. pipeline() propagates errors diff --git a/api/invisibility-test.js b/api/invisibility-test.js index 08b74026..f91c1d3a 100644 --- a/api/invisibility-test.js +++ b/api/invisibility-test.js @@ -1,5 +1,5 @@ import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; // If length is not 28 and is not a combination of letters and numbers, return false function isValidUserID(userID) { diff --git a/api/ip-blocklist.js b/api/ip-blocklist.js new file mode 100644 index 00000000..6b01241b --- /dev/null +++ b/api/ip-blocklist.js @@ -0,0 +1,53 @@ +// GET /api/ipblocklist?ip= — which blocklists an IP is on, from the private +// IPCheck.ing API (`/iphitlist`: live DNSBL lookups plus static threat lists). +// `?ip` is validated by requirePublicIP(). Sign-in is enforced upstream, so +// the caller's headers (auth token) are forwarded as-is. + +import { fetchUpstream } from '../common/fetch-with-timeout.js'; +import logger from '../server/logger.js'; + +// A cold DNSBL pass queries dozens of zones under per-zone deadlines; well +// past the default 8s, and the frontend waits a little longer than this. +export const IP_BLOCKLIST_TIMEOUT_MS = 30000; + +export default async (req, res) => { + const apiKey = process.env.IPCHECKING_API_KEY; + const apiEndpoint = process.env.IPCHECKING_API_ENDPOINT; + if (!apiKey || !apiEndpoint) { + return res.status(500).json({ error: 'API key is missing' }); + } + + const url = new URL(`${apiEndpoint}/iphitlist`); + url.searchParams.set('ip', req.query.ip); + url.searchParams.set('key', apiKey); + + try { + const apiResponse = await fetchUpstream(url, { + headers: { ...req.headers }, + timeoutMs: IP_BLOCKLIST_TIMEOUT_MS, + }); + + // Monthly quota exhausted: not a failure — pass status + code through + // so the frontend shows the quota hint. + if (apiResponse.status === 429) { + const errorData = await apiResponse.json().catch(() => ({})); + return res.status(429).json({ error: errorData.error || 'Monthly quota exceeded', code: 'quota_exceeded' }); + } + + // Sign-in problems are the visitor's state, not a failure: pass the + // status through so the frontend prompts sign-in. + if (apiResponse.status === 401 || apiResponse.status === 403) { + const errorData = await apiResponse.json().catch(() => ({})); + return res.status(apiResponse.status).json({ error: errorData.message || 'Sign in required' }); + } + + if (!apiResponse.ok) { + throw new Error(`API responded with status: ${apiResponse.status}`); + } + + res.json(await apiResponse.json()); + } catch (error) { + logger.error({ err: error, ip: req.query.ip }, 'ip-blocklist upstream request failed'); + res.status(500).json({ error: error.message }); + } +}; diff --git a/api/ip-sb.js b/api/ip-sb.js index 2cfeb994..7cc44ca0 100644 --- a/api/ip-sb.js +++ b/api/ip-sb.js @@ -2,7 +2,7 @@ // Token-free upstream; normalizes the response into the canonical geo // shape via the shared makeGeoHandler factory. -import { makeGeoHandler } from '../common/geo-handler.js'; +import { makeGeoHandler } from '../server/geo-handler.js'; function buildUrl(req) { const ipAddress = req.query.ip; diff --git a/api/ip2location-io.js b/api/ip2location-io.js index 36ff5417..e2b25ad6 100644 --- a/api/ip2location-io.js +++ b/api/ip2location-io.js @@ -2,7 +2,7 @@ // Picks a random API key and normalizes the response into the canonical // geo shape via the shared makeGeoHandler factory. -import { makeGeoHandler } from '../common/geo-handler.js'; +import { makeGeoHandler } from '../server/geo-handler.js'; function buildUrl(req) { const ipAddress = req.query.ip; diff --git a/api/ipapi-com.js b/api/ipapi-com.js index 2760b2a6..b6689246 100644 --- a/api/ipapi-com.js +++ b/api/ipapi-com.js @@ -2,7 +2,7 @@ // Forwards the optional ?lang and normalizes the response into the // canonical geo shape via the shared makeGeoHandler factory. -import { makeGeoHandler } from '../common/geo-handler.js'; +import { makeGeoHandler } from '../server/geo-handler.js'; function buildUrl(req) { const ipAddress = req.query.ip; diff --git a/api/ipapi-is.js b/api/ipapi-is.js index 4defa0c4..a9d81947 100644 --- a/api/ipapi-is.js +++ b/api/ipapi-is.js @@ -2,7 +2,7 @@ // Picks a random API key and normalizes the response into the canonical // geo shape (plus isHosting / isProxy) via the shared makeGeoHandler factory. -import { makeGeoHandler } from '../common/geo-handler.js'; +import { makeGeoHandler } from '../server/geo-handler.js'; function buildUrl(req) { const ipAddress = req.query.ip; diff --git a/api/ipcheck-ing.js b/api/ipcheck-ing.js index 152b3624..03a361fd 100644 --- a/api/ipcheck-ing.js +++ b/api/ipcheck-ing.js @@ -1,5 +1,5 @@ import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; export default async (req, res) => { // Presence, validity and public routability guaranteed by the diff --git a/api/ipinfo-io.js b/api/ipinfo-io.js index 4e25bb63..d044f079 100644 --- a/api/ipinfo-io.js +++ b/api/ipinfo-io.js @@ -3,7 +3,7 @@ // into the canonical geo shape via the shared makeGeoHandler factory. import getCountryName from '../common/country-name.js'; -import { makeGeoHandler } from '../common/geo-handler.js'; +import { makeGeoHandler } from '../server/geo-handler.js'; function buildUrl(req) { const ipAddress = req.query.ip; diff --git a/api/mac-checker.js b/api/mac-checker.js index 057e11ca..1f9e84aa 100644 --- a/api/mac-checker.js +++ b/api/mac-checker.js @@ -1,11 +1,11 @@ // /api/macchecker?mac= — MAC Lookup against the local IEEE registries -// (common/oui-db.js). `mac` is a full address or a vendor prefix of 6–12 +// (server/datasets/oui-db.js). `mac` is a full address or a vendor prefix of 6–12 // hex digits (common/mac-input.js). The answer names the block that covers // it, its range and the first-octet flags; an address no block covers is // still a 200 with `found: false`. import { normalizeMacQuery } from '../common/mac-input.js'; -import { lookupMac } from '../common/oui-db.js'; +import { lookupMac } from '../server/datasets/oui-db.js'; // `lookup` is injectable so tests can run the handler over fixture registries. export const createMacChecker = (lookup) => async (req, res) => { diff --git a/api/maxmind.js b/api/maxmind.js index 8c1d68bb..5990d3d9 100644 --- a/api/maxmind.js +++ b/api/maxmind.js @@ -1,5 +1,5 @@ -import { lookupMaxMind } from '../common/maxmind-service.js'; -import logger from '../common/logger.js'; +import { lookupMaxMind } from '../server/datasets/maxmind-service.js'; +import logger from '../server/logger.js'; export default (req, res) => { // Presence, validity and public routability guaranteed by the diff --git a/api/ooni-blocking.js b/api/ooni-blocking.js index 25f53fbd..125d7c5c 100644 --- a/api/ooni-blocking.js +++ b/api/ooni-blocking.js @@ -1,7 +1,7 @@ // /api/ooni-blocking — per-country blocking view for a domain over the last // 30 days, built from OONI's open aggregation API (web_connectivity test, // probe_cc × blocking_type axes). Powers the default view of the frontend -// CensorshipCheck tool; classification lives in common/ooni-blocking.js. +// CensorshipCheck tool; classification lives in server/ooni-blocking.js. // // OONI matches `domain` against the exact tested hostname, so a single query // misses most of the dataset (e.g. `bbc.com` has ~6 rows while `www.bbc.com` @@ -13,8 +13,8 @@ // `cacheable()` and CF absorbs repeat lookups of popular domains. import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import { classifyOoniCountries, OONI_WINDOW_DAYS } from '../common/ooni-blocking.js'; -import logger from '../common/logger.js'; +import { classifyOoniCountries, OONI_WINDOW_DAYS } from '../server/ooni-blocking.js'; +import logger from '../server/logger.js'; const OONI_AGGREGATION_URL = 'https://api.ooni.io/api/v1/aggregation'; diff --git a/api/persona.js b/api/persona.js index 69e82f71..8e02331a 100644 --- a/api/persona.js +++ b/api/persona.js @@ -7,7 +7,7 @@ // ids and enums, and the front end renders its own four-language copy. import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; // Headers that describe *this* hop rather than the caller, dropped before the // request is rebuilt. diff --git a/api/sentry-tunnel.js b/api/sentry-tunnel.js index 70133a2c..8e1efa32 100644 --- a/api/sentry-tunnel.js +++ b/api/sentry-tunnel.js @@ -7,7 +7,7 @@ // only when VITE_SENTRY_DSN_FRONTEND is set. import { fetchUpstream } from '../common/fetch-with-timeout.js'; import { isValidIP } from '../common/valid-ip.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; // The envelope header is the first newline-delimited JSON line and carries // the DSN the SDK was configured with. Returns a URL or null. Exported for diff --git a/api/service-status.js b/api/service-status.js index 0bc56929..e4df5073 100644 --- a/api/service-status.js +++ b/api/service-status.js @@ -1,7 +1,7 @@ // /api/service-status — the "Service Status" section's read endpoints. // // Both handlers serve slices of the in-memory snapshot maintained by -// common/service-status-store.js (a background timer refreshes every provider +// server/service-status-store.js (a background timer refreshes every provider // on a fixed 5-minute schedule). Neither touches an upstream — request volume // has no effect on upstream load. Splitting overview vs per-provider detail // keeps the initial page load light: the detail (sub-services + incidents) is @@ -13,7 +13,7 @@ // The detail handler's `id` query param is whitelisted by // requireValidProviderId() in backend-server.js. -import { getServiceStatusOverview, getProviderDetail } from '../common/service-status-store.js'; +import { getServiceStatusOverview, getProviderDetail } from '../server/service-status-store.js'; // Overview: every provider's status light, no heavy detail arrays. export default async (req, res) => { diff --git a/api/share-report.js b/api/share-report.js index a801c1f3..370cde0a 100644 --- a/api/share-report.js +++ b/api/share-report.js @@ -17,7 +17,7 @@ import crypto from 'node:crypto'; import { fetchUpstream } from '../common/fetch-with-timeout.js'; import { validateReport, REPORT_TTL_DAYS, REPORT_MAX_BYTES } from '../common/report-schema.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; // --- env / KV plumbing ------------------------------------------------------- diff --git a/api/update-user-achievement.js b/api/update-user-achievement.js index e8fb7a24..4ea5fae5 100644 --- a/api/update-user-achievement.js +++ b/api/update-user-achievement.js @@ -1,5 +1,5 @@ import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; export default async (req, res) => { // defensive; app.put() in backend-server.js already gates method diff --git a/backend-server.js b/backend-server.js index 011ae227..62aed4e0 100644 --- a/backend-server.js +++ b/backend-server.js @@ -6,10 +6,10 @@ import { fileURLToPath } from 'url'; import { slowDown } from 'express-slow-down' import rateLimit from 'express-rate-limit'; import pinoHttp from 'pino-http'; -import logger from './common/logger.js'; +import logger from './server/logger.js'; import { requireReferer, requirePublicIP, requireValidPrefix, requireValidASN, requireValidDomain, requireValidProviderId, - requireValidRecordType, requireValidReportId, normalizeAsnQuery } from './common/guards.js'; -import { withTimeZone } from './common/ip-timezone.js'; + requireValidRecordType, requireValidReportId, normalizeAsnQuery } from './server/guards.js'; +import { withTimeZone } from './server/ip-timezone.js'; // Backend APIs import mapHandler from './api/google-map.js'; @@ -23,7 +23,7 @@ import ipsbHandler from './api/ip-sb.js'; import maxmindHandler from './api/maxmind.js'; // Others import cfRadarHandler from './api/cf-radar.js'; -import { RADAR_VIEWS, isCompleteRadarAnswer } from './common/cf-radar.js'; +import { RADAR_VIEWS, isCompleteRadarAnswer } from './server/cf-radar.js'; import asnHistoryHandler from './api/asn-history.js'; import asnConnectivityHandler from './api/asn-connectivity.js'; import ooniBlockingHandler from './api/ooni-blocking.js'; @@ -37,27 +37,28 @@ import getWhois from './api/get-whois.js'; import sentryTunnelHandler from './api/sentry-tunnel.js'; import createReportHandler, { getReport as getReportHandler } from './api/share-report.js'; import invisibilitytestHandler from './api/invisibility-test.js'; +import ipBlocklistHandler from './api/ip-blocklist.js'; import macChecker from './api/mac-checker.js'; import githubStarsHandler from './api/github-stars.js'; import personaEvaluateHandler from './api/persona.js'; import asnProfileHandler from './api/asn-profile.js'; -import { isCompleteProfile } from './common/asn-profile.js'; +import { isCompleteProfile } from './server/asn-profile.js'; // User import validateConfigs from './api/configs.js'; import getUserinfo from './api/get-user-info.js'; import updateUserAchievement from './api/update-user-achievement.js'; -import { reloadMaxMindDatabases, isMaxMindReady, getMaxMindDbPaths } from './common/maxmind-service.js'; -import { bootstrapDatasets, startDatasetScheduler, watchDatasets } from './common/dataset-updater.js'; -import { datasets } from './common/datasets.js'; +import { reloadMaxMindDatabases, isMaxMindReady, getMaxMindDbPaths } from './server/datasets/maxmind-service.js'; +import { bootstrapDatasets, startDatasetScheduler, watchDatasets } from './server/datasets/dataset-updater.js'; +import { datasets } from './server/datasets/datasets.js'; import { bootstrapServiceStatus, startServiceStatusPolling, isServiceStatusPrimed, -} from './common/service-status-store.js'; -import { isAsRelLoaded } from './common/as-rel-db.js'; -import { isAsOrgLoaded } from './common/as-org-db.js'; -import { isOuiLoaded } from './common/oui-db.js'; -import { runOfflineBootstrap, requireOfflineData, isOfflineBootstrapping } from './common/offline-data.js'; -import { cacheable } from './common/cache-control.js'; -import { initUpstreamUserAgent } from './common/upstream-ua.js'; +} from './server/service-status-store.js'; +import { isAsRelLoaded } from './server/datasets/as-rel-db.js'; +import { isAsOrgLoaded } from './server/datasets/as-org-db.js'; +import { isOuiLoaded } from './server/datasets/oui-db.js'; +import { runOfflineBootstrap, requireOfflineData, isOfflineBootstrapping } from './server/offline-data.js'; +import { cacheable } from './server/cache-control.js'; +import { initUpstreamUserAgent } from './server/upstream-ua.js'; dotenv.config({ quiet: true }); initUpstreamUserAgent(); @@ -216,17 +217,17 @@ if (speedLimitSet !== 0) { app.use(express.json({ limit: '500kb' })); // Default every /api/* response to no-store. Routes that want edge caching -// declare it explicitly via `cacheable(maxAge)` (common/cache-control.js). +// declare it explicitly via `cacheable(maxAge)` (server/cache-control.js). app.use('/api', (req, res, next) => { res.setHeader('Cache-Control', 'no-store'); next(); }); // Global referer gate for all /api/* routes. Handlers no longer repeat this -// check individually — see common/guards.js. +// check individually — see server/guards.js. app.use('/api', requireReferer); -// Readiness gates for routes reading local data (common/offline-data.js): +// Readiness gates for routes reading local data (server/offline-data.js): // 503 while that data is still downloading at boot, a no-op afterwards. const needsMaxMind = requireOfflineData([isMaxMindReady]); const needsAsGraph = requireOfflineData([isAsRelLoaded, isAsOrgLoaded]); @@ -275,7 +276,7 @@ app.get('/api/asn-profile', requireValidASN(), cacheable(SEVEN_DAYS_CACHE, { degradedMaxAge: () => (isOfflineBootstrapping() ? 0 : ONE_DAY_CACHE), }), asnProfileHandler); // All Cloudflare Radar data rides one route; `?view=` picks the dataset and -// the TTL comes from that view's registry entry (common/cf-radar.js) — 7d +// the TTL comes from that view's registry entry (server/cf-radar.js) — 7d // for the ASN summary and prefix list (same as /api/asn-profile), 30d for // country traffic, 1h for the outage feed. A partial answer (some upstream // calls failed, or prefix countries MaxMind couldn't fully work out) is @@ -294,6 +295,7 @@ app.get('/api/ipchecking', requirePublicIP(), withTimeZone(), ipCheckingHandler) app.get('/api/dnsresolver', requireValidDomain('hostname'), requireValidRecordType(), dnsResolver); app.get('/api/dnsleaktest/session/:token', dnsLeakGetResult); app.get('/api/invisibility', invisibilitytestHandler); +app.get('/api/ipblocklist', requirePublicIP(), ipBlocklistHandler); app.get('/api/getuserinfo', getUserinfo); app.put('/api/updateuserachievement', updateUserAchievement); // Shared reports stay no-store: an edge cache could serve a report past its diff --git a/common/asn-input.js b/common/asn-input.js index 9e9a1790..13824993 100644 --- a/common/asn-input.js +++ b/common/asn-input.js @@ -1,7 +1,7 @@ -// The AS-number grammar shared by the ASN guards (common/guards.js) and the +// The AS-number grammar shared by the ASN guards (server/guards.js) and the // tools that take a typed AS number (Whois, ASN Profile — via the -// frontend/utils/asn-input.js bridge): up to ten digits, AS1 … AS4294967295, -// `AS` prefix case-insensitive. +// frontend/utils/ip/asn-input.js bridge): up to ten digits, AS1 … +// AS4294967295, `AS` prefix case-insensitive. export const MAX_ASN = 4294967295; diff --git a/common/bgp-prefix.js b/common/bgp-prefix.js index 54de1ed7..76e62bec 100644 --- a/common/bgp-prefix.js +++ b/common/bgp-prefix.js @@ -10,7 +10,7 @@ const HEX_GROUP = /^[0-9a-fA-F]{1,4}$/; // Expand an IPv6 address (with optional `::` compression) into 8 hextet // strings. Returns null on any structural failure. Also consumed by -// common/rdap.js for CIDR containment math. +// server/rdap.js for CIDR containment math. export function expandIPv6(ip) { const halves = ip.split('::'); if (halves.length > 2) return null; diff --git a/common/fetch-with-timeout.js b/common/fetch-with-timeout.js index bdf146a5..444939ab 100644 --- a/common/fetch-with-timeout.js +++ b/common/fetch-with-timeout.js @@ -36,7 +36,7 @@ export async function fetchWithTimeout(url, init = {}) { } // Optional User-Agent for fetchUpstream calls. Backend boot injects a -// project-identifying UA (see common/upstream-ua.js) because some upstream +// project-identifying UA (see server/upstream-ua.js) because some upstream // WAFs hard-block undici's default `User-Agent: node`. Injection keeps this // file browser-safe: the frontend bundle never touches fs / process. let upstreamUserAgent = null; diff --git a/common/firebase-auth-proxy.js b/common/firebase-auth-proxy.js new file mode 100644 index 00000000..15813e47 --- /dev/null +++ b/common/firebase-auth-proxy.js @@ -0,0 +1,11 @@ +// Firebase Auth handler proxy: `/__/auth/*` on the site's own origin → +// `.firebaseapp.com`, mounted by frontend-server.js (production) +// and vite.config.js (dev). Why: api/AGENTS.md "Firebase Auth handler proxy". + +export const FIREBASE_AUTH_PROXY_PATH = '/__/auth'; + +// null without a project id: no Firebase, no proxy. +export const firebaseAuthProxyTarget = (projectId) => { + const id = typeof projectId === 'string' ? projectId.trim() : ''; + return id ? `https://${id}.firebaseapp.com` : null; +}; diff --git a/common/locale-registry.js b/common/locale-registry.js index 634e8f50..5a711045 100644 --- a/common/locale-registry.js +++ b/common/locale-registry.js @@ -3,7 +3,7 @@ // // The back end takes no part in that: which languages upstream *data* comes in // is a separate set owned by the source itself (SUPPORTED_LANGS in -// common/maxmind-service.js), and every `?lang` consumer resolves an unfamiliar +// server/datasets/maxmind-service.js), and every `?lang` consumer resolves an unfamiliar // tag onto its own family instead of rejecting it. A new UI locale is a // front-end-only change. diff --git a/common/mac-input.js b/common/mac-input.js index 202bc211..e727d6e8 100644 --- a/common/mac-input.js +++ b/common/mac-input.js @@ -1,8 +1,8 @@ // MAC Lookup query parsing, shared by the /api/macchecker handler and the -// MacChecker form (via frontend/utils/mac-input.js). A query is a full -// 48-bit address or any leading part of one, down to a 24-bit vendor prefix, -// in the usual notations: colon / hyphen pairs, Cisco dotted quads, or bare -// hex. +// MacChecker form (via frontend/utils/features/mac-input.js). A query is a +// full 48-bit address or any leading part of one, down to a 24-bit vendor +// prefix, in the usual notations: colon / hyphen pairs, Cisco dotted quads, +// or bare hex. export const MAC_HEX_LENGTH = 12; export const MAC_PREFIX_MIN_HEX = 6; diff --git a/common/report-schema.js b/common/report-schema.js index 00c3fd60..2c3e6a56 100644 --- a/common/report-schema.js +++ b/common/report-schema.js @@ -25,7 +25,7 @@ export const REPORT_TTL_DAYS = [1, 3, 7]; export const REPORT_MAX_BYTES = 256 * 1024; // Anonymity verdict codes for ipinfo cards — made by -// utils/transform-ip-data.js, shared by the builder whitelist and the schema +// utils/ip/transform-ip-data.js, shared by the builder whitelist and the schema // enum below. export const ANONYMITY_CODES = [ 'tor', 'relay', 'residential', 'suspected_residential', @@ -33,7 +33,7 @@ export const ANONYMITY_CODES = [ ]; // Connectivity status codes — single source shared by the recording component -// (ConnectivityTest.vue), the builder whitelist (utils/report-builders.js) +// (ConnectivityTest.vue), the builder whitelist (utils/report/report-builders.js) // and the schema enum below, so a rename can't drift them apart. export const CONNECTIVITY_STATUS = { OK: 'ok', @@ -165,7 +165,7 @@ const SECTION_SPECS = { connectivity: obj({ testedAt: isoDate(), // 72 = 7 built-in targets + the 60-target custom/import cap, plus - // headroom; matches the builder's slice in utils/report-builders.js. + // headroom; matches the builder's slice in utils/report/report-builders.js. targets: arr(72, obj({ id: str(48), name: str(64), @@ -268,6 +268,19 @@ const SECTION_SPECS = { rtt: opt(int(0, 600000)), }))), }), + // Ad & Tracker Blocking Test: per-category request counts only. Group + // ids stay capped strings — the probe list lives in the front end and + // changes with it. + adblocktest: obj({ + testedAt: isoDate(), + groups: arr(16, obj({ + id: str(16), + total: int(0, 1024), + blocked: int(0, 1024), + open: int(0, 1024), + unknown: int(0, 1024), + })), + }), invisibility: obj({ testedAt: isoDate(), ip: opt(ipValue()), diff --git a/common/valid-ip.js b/common/valid-ip.js index f1627455..d9f9ad99 100644 --- a/common/valid-ip.js +++ b/common/valid-ip.js @@ -35,6 +35,10 @@ function isIPv6(ip) { return typeof ip === 'string' && ip.includes(':'); } +// Validate an IP of one specific version — safe on untrusted input. +const isValidIPv4 = (ip) => isValidIP(ip) && !isIPv6(ip); +const isValidIPv6 = (ip) => isValidIP(ip) && isIPv6(ip); + // Validate if a string is a syntactically plausible domain name. // Matches the hostname pattern used by DnsResolver / Whois / CensorshipCheck: // labels of [a-z0-9-], at least one dot, and a TLD of 2+ letters. Any label @@ -119,4 +123,4 @@ const isUsablePublicIP = (ip) => { return !(isIPv6(ip) ? isReservedV6(ip) : isReservedV4(ip)); }; -export { isValidIP, isIPv6, isValidDomain, isUsablePublicIP }; +export { isValidIP, isValidIPv4, isValidIPv6, isIPv6, isValidDomain, isUsablePublicIP }; diff --git a/frontend-server.js b/frontend-server.js index 3a5a7c7c..6b137255 100644 --- a/frontend-server.js +++ b/frontend-server.js @@ -4,7 +4,8 @@ import express from 'express'; import path from 'path'; import { fileURLToPath } from 'url'; import { createProxyMiddleware } from 'http-proxy-middleware'; -import logger from './common/logger.js'; +import logger from './server/logger.js'; +import { FIREBASE_AUTH_PROXY_PATH, firebaseAuthProxyTarget } from './common/firebase-auth-proxy.js'; dotenv.config({ quiet: true }); @@ -21,6 +22,18 @@ frontendApp.use('/api', createProxyMiddleware({ changeOrigin: true })); +// Firebase's auth handler served first-party (api/AGENTS.md "Firebase Auth +// handler proxy"); ahead of the static layer and SPA fallback so it never +// resolves to index.html. +const firebaseAuthTarget = firebaseAuthProxyTarget(process.env.VITE_FIREBASE_PROJECT_ID); +if (firebaseAuthTarget) { + frontendApp.use(FIREBASE_AUTH_PROXY_PATH, createProxyMiddleware({ + target: `${firebaseAuthTarget}${FIREBASE_AUTH_PROXY_PATH}`, + changeOrigin: true + })); + logger.info(`🛡️ Firebase Auth handler proxied: ${FIREBASE_AUTH_PROXY_PATH}/* → ${firebaseAuthTarget}`); +} + // Set static file directory. // Cache-Control is set per-asset class so the static layer behaves well // even when no CDN sits in front of it (CF in production sets its own diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index bfa48f58..a4f2bce8 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -13,9 +13,10 @@ over copied-in shadcn-vue primitives. No TypeScript, no `dark:` dual pairs. ``` frontend/ ├── App.vue / main.js / store.js / router/ / locales/ / style/style.css -├── firebase-init.js ← env-gated lazy Firebase Auth (boot path: utils/auth-hint.js) +├── firebase-init.js ← env-gated lazy Firebase Auth (boot path: utils/auth-hint.js; +│ PWA / blocked-popup redirect sign-in: utils/auth-redirect.js) ├── sentry-init.js ← env-gated Sentry (see "Error monitoring") -├── data/ ← static config (tools registry drives router+cards+drawer) +├── data/ ← static config (tools registry drives pages+cards+nav) ├── lib/ · utils/ · composables/ ← see "Helper placement" └── components/ ← sections + ip-infos/ advanced-tools/ report/ widgets/ svgicons/ ui/ ``` @@ -26,10 +27,14 @@ Every file opens with a header comment stating its purpose — read those. - **Composition API** everywhere; no Options API. Alias `@` → `frontend/`. - **Shared-with-backend helpers live in `common/`**, re-exported through a thin - `utils/` bridge so consumers keep `@/utils/...` imports (`utils/valid-ip.js`). + `utils/` bridge so consumers keep `@/utils/...` imports (`utils/ip/valid-ip.js`). - **Helper placement:** Vue reactivity / lifecycle → `composables/` (`useXxx`); otherwise `utils/` (never `use-` prefixed). `lib/` stays shadcn-only. A pure function next to a composable exports from that composable's file. +- **Inside `utils/`:** app-wide plumbing at its root; IP / ASN data shaping + and validation → `ip/`; the report pipeline → `report/`; the pure layer of + one feature (a tool, a section, Pulse) → `features/`; per-source probe + modules keep their own dirs (`getips/`, `dnsleaks/`, `persona/`). ### Achievements are event-driven @@ -39,7 +44,7 @@ events → slugs, `composables/use-achievement-engine.js` owns all guards (rules wait for the remote snapshot; pre-sync hits parked). New achievement = entry + rule + (only if no suitable event exists) a new event. The shareable report rides the same bus: `:finished` → `use-report-collector.js` → -`utils/report-builders.js` → sections whitelisted by `common/report-schema.js`. +`utils/report/report-builders.js` → sections whitelisted by `common/report-schema.js`. New reportable test = event + builder + schema entry in one change; new result semantics = builder whitelist + schema enum too (builders fail soft, fixtures are frozen: drift = quietly missing fields). Report links are public: the @@ -56,18 +61,39 @@ reserved `auth` / `quota` / `input` (the bus adds `unavailable` / `timeout`). Cross-component triggers use the bus, never template refs (refs stay for UI chrome); tools register at setup, callers `waitForAppCommand` first. -### Advanced Tools gates decide listing only +### Advanced Tools are pages, kept alive -`data/tools.js` gates (`requiresOriginalSite`, `requiresConfig: ''`, -e.g. `asn` → `cloudFlare`) are read only via `isToolAvailable()`, by the card -grid and the nav; deep links (`?tool=`, `/tools/:slug`) are never gated. +- Every tool is a page at `/tools/:slug` (`ToolPage.vue` over `data/tools.js`), + and every opener routes there: cards are real `` (a plain click + `router.push`es, modified clicks stay the browser's), Nav, shortcuts + (`Advanced.vue`'s exposed `openTool`), in-app links. `/?tool=` is only + a replace-redirect in the router's `beforeEach` (`utils/legacy-tool-link.js`). +- Gates (`requiresOriginalSite`, `requiresConfig: ''`, e.g. `asn` → + `cloudFlare`) decide listing only, via `isToolAvailable()` / `listedTools()` + (card grid, Nav); a `/tools/:slug` link is never gated. +- `App.vue` keeps Home and `ToolPage` alive (`KeepAlive`); `ToolPage` keeps up + to 8 tools alive per slug, dropped on sign-out / account switch + (`utils/tool-cache.js`). Back / forward is plain history; the breadcrumb's + Home steps back when the previous entry is Home (`utils/back-target.js`), so + Home returns as left, scroll included. +- A cached page never unmounts, so whatever acts on the route, the document + head, window events or the store's one-shot triggers gates on + `use-route-active.js` (`useRouteActive` / `useActiveValue` / + `useActiveEventListener`), not on mount / unmount. Overlays on a page being + left are closed by `use-overlay-shortcuts.js` — their portal would outlive it; + App.vue's own (store `openSheet`, the `User` dialog) close on a path change. +- Homepage state reaches a tool page through the store, the app-events + collectors and the command bus: a cached Home keeps its commands registered, + so a caller dispatches in place and goes home first only when + `hasAppCommand()` says Home never mounted (Persona Check). ### ASN Profile - One `/api/asn-profile` request, one loading state, then the whole page; a section (reputation included — no `configs.originalSite` check) renders only - when its `status` is `ok`. `ASNConnectivity.vue` gets `:expandable="false" - :bordered="false"`; its defaults keep IPCard / QueryIP unchanged. + when its `status` is `ok`. `ASNConnectivity.vue` gets `:bordered="false"` + (the card draws the border) and keeps its expand-to-drawer; its defaults + keep IPCard / QueryIP unchanged. - `peeringdb` is operator self-reporting: vocabularies → locale keys (`PEERING_ENUMS`), names / cities as written. `ASN_PROFILE_TIMEOUT_MS` stays above the largest backend section deadline (tested). @@ -77,7 +103,7 @@ grid and the nav; deep links (`?tool=`, `/tools/:slug`) are never gated. ### Overlays take no keyboard shortcuts One dispatcher (`utils/shortcut.js`) over the map `use-shortcuts.js` registers -(home route only; Home clears it on unmount; `registerShortcuts()` replaces it), +(home route only; Home clears it when left; `registerShortcuts()` replaces it), suspended while an overlay is open — keyed off form: the `ui/` roots `Dialog` / `Sheet` / `Drawer` call `use-overlay-shortcuts.js`, so anything built on them inherits it; overlays nest; Esc and native scrolling still work. @@ -99,7 +125,9 @@ masked; in the privacy policy). No backend 5xx capture; envelopes: `/api/monitor Tailwind only when neither fits. Keep on upstream syncs: `Spinner` + `ToolLoadingSkeleton`, `toggle` / `toggle-group`'s `primary` pressed pair, the overlay roots' shortcut suspension, `select`'s trigger geometry (`py-1` + flex -value span, not `-webkit-box` — Safari shifts it ~2px). +value span, not `-webkit-box` — Safari shifts it ~2px). CLI trap: `shadcn-vue +add` here writes `import { cn } from "cn"` and installs the unrelated `cn` +package — drop the dependency, import from `@/lib/utils`. ### Design tokens @@ -131,17 +159,48 @@ Every "business state → color" mapping goes through `use-status-tone.js` `INLINE_TIERS`; `:max-lines="2"` on heroes); no length-threshold helpers. - **Filter tags** — `ToggleGroup :spacing="2"` of detached `h-7 rounded-full` pills, `w-full flex-wrap` (IPHistory, DnsResolver); never `spacing=0`. -- **Shareable tool input** — watch `route.query.q` (immediate, so mount and - later changes in an open drawer both run), `router.replace` it on every run - (AsnProfile); both `/tools/` and `?tool=` URLs. +- **Shareable tool input** — watch `useActiveValue(() => route.query.q, { + pathOf: () => route.path })` (immediate, so mount, later changes and a `q` + changed while cached all run; another page's `q` never does), `router.replace` + it on every run (AsnProfile); the URL is `/tools/?q=`. A `q` runs + the tool only when a run needs no sign-in or quota (AsnProfile, + IpCalculator); otherwise it just prefills (IpBlocklist). - **Fixed option sets** — a closed list wider than one line is a `Select`. - **Qualifier + input + run** — `Select` + `Input` in one `ButtonGroup`, the run Button in a second (DnsResolver); trigger `w-auto shrink-0`; never wraps. - **Tables vs lists** — real per-column header semantics → ``; otherwise a bordered `
    `. - **Dialog header** — the `` primitive. -- **Drawer vs Sheet** — bottom Drawer for Advanced Tools and full-bleed - expansions of an inline visual; side panels use `Sheet`. +- **Site navigation** — one `Nav.vue` in `App.vue` above every route (fixed; + `index.html` pads the body for it), never per page; `User` / `Preferences` + sit beside it, one each. What a click does on the current route is + `resolveNavTarget()` (`utils/nav-target.js`: scroll on Home, go home then + scroll elsewhere, a tool → its page), carried out by `use-nav-target.js`; + no section is highlighted off Home. + - The link row collapses to the hamburger + Sheet when `use-nav-overflow.js` + measures it doesn't fit (no breakpoint); `isMobile` only drives phone behaviour. +- **Footer** — four columns on the Advanced Tools grid: brand / Project / Docs + & Support / Clients (only the CLI entry is gated, on the curl domains); + never repeats on-page content (tools, sections); About / Changelog / + Acknowledgement open in its About sheet, the CLI in its own dialog + (`Footer.vue`). +- **Page breadcrumb** — every page but Home opens its body with + `` above the `

    ` (← Home / … / current page): + real links, Home via `resolveBackTarget()`, section crumbs via the Nav's + helper, a bare last crumb `aria-current="page"`. A tool page ends on the + Advanced Tools crumb: its `

    ` already names the tool. +- **Page transitions** — View Transitions hooked in the router's + `beforeResolve` (`utils/page-transition.js`: Home → page slides forward, + → Home back, page → page fades; keyframes in `style/style.css`); none on a + query-only change, the first load, reduced motion or a history step the + browser animated itself. Only the Nav has a `view-transition-name` (it + stays still). After a history step, `utils/revive-animations.js` replays + the page's running CSS animations: iOS swipe-back leaves them frozen. +- **Responsive hide** — `.hidden` is `!important` (`style/style.css`), so + `hidden sm:flex` never shows: write `max-sm:hidden`. +- **Drawer vs Sheet** — bottom Drawer only for a full-bleed expansion of an + inline visual (ASNConnectivity's graph); side panels use `Sheet`; a tool is + a page, never an overlay. - **Motion** — hover lift `transition-transform duration-300 ease-out hover:-translate-y-1.5`; loading is ``, never pulse-dots. diff --git a/frontend/App.vue b/frontend/App.vue index bf4502b4..31b678a5 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -1,9 +1,19 @@ @@ -210,28 +123,4 @@ defineExpose({ :global(.dark) .jn-adv-card:hover .jn-emoji { text-shadow: 0 0 10pt rgb(255 255 255 / 0.15); } - -/* Drawer content area width (desktop) */ -.jn-canvas-width { - width: fit-content; - margin: auto; - max-width: 1400px; -} - -.jn-drawer-header { - border-bottom: 1px solid var(--border); -} - -/* Drawer root container needs flex-col, so that the header is fixed + content scrollable */ -.jn-tools-drawer { - display: flex; - flex-direction: column; -} - -/* Full screen toggle height transition */ -:global(.jn-tools-drawer) { - transition: - transform 0.5s cubic-bezier(0.32, 0.72, 0, 1), - height 0.3s cubic-bezier(0.32, 0.72, 0, 1) !important; -} diff --git a/frontend/components/ConnectivityTest.vue b/frontend/components/ConnectivityTest.vue index 008d36fb..f6ca1a26 100644 --- a/frontend/components/ConnectivityTest.vue +++ b/frontend/components/ConnectivityTest.vue @@ -160,9 +160,9 @@ import { useI18n } from 'vue-i18n'; import { trackEvent } from '@/utils/analytics'; import { emitAppEvent, waitForAppEvent } from '@/utils/app-events'; import { useAppCommand } from '@/composables/use-app-command.js'; -import { CONNECTIVITY_STATUS } from '@/utils/report-schema.js'; +import { CONNECTIVITY_STATUS } from '@/utils/report/report-schema.js'; import { TILE_PREVIEW, faviconPath, MINE_LIST_ID } from '@/data/connectivity-import-lists.js'; -import { siteUrlOf, removeMember } from '@/utils/connectivity-lists.js'; +import { siteUrlOf, removeMember } from '@/utils/features/connectivity-lists.js'; import { JnTooltip } from '@/components/ui/tooltip'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; @@ -386,7 +386,7 @@ const checkConnectivityHandler = async (test, onTestComplete = () => { }, isManu const testTime = Math.round(performance.now() - beginTime); test.status = t('connectivity.StatusAvailable'); // Locale-free twin of the localized status label, consumed by the report - // builder (utils/report-builders.js) — the label itself can't be mapped + // builder (utils/report/report-builders.js) — the label itself can't be mapped // back to an enum without string comparison against t() output. test.statusCode = CONNECTIVITY_STATUS.OK; test.mintime = test.mintime === 0 ? testTime : Math.min(test.mintime, testTime); diff --git a/frontend/components/DnsLeaksTest.vue b/frontend/components/DnsLeaksTest.vue index 58c9aedb..cb47b4e6 100644 --- a/frontend/components/DnsLeaksTest.vue +++ b/frontend/components/DnsLeaksTest.vue @@ -107,13 +107,13 @@ import FitText from '@/components/widgets/FitText.vue'; import InfoBanner from '@/components/widgets/InfoBanner.vue'; import { INLINE_TIERS } from '@/composables/use-fit-text.js'; import { - ipApi, bashws, myipstack, fastly, browserleaks, surfshark, ipleak, + ipApi, bashws, myipstack, fastly, browserleaks, surfshark, ipleak, every1dns, createDnsLeakRunner, } from '@/utils/dnsleaks'; // All providers in priority order: the first SLOT_COUNT hold a card each, // the rest are standbys. New provider = file under utils/dnsleaks + append. -const PROVIDERS = [ipApi, surfshark, myipstack, fastly, bashws, browserleaks, ipleak]; +const PROVIDERS = [ipApi, every1dns, myipstack, fastly, surfshark, bashws, browserleaks, ipleak]; const SLOT_COUNT = 4; const ACTIVE = PROVIDERS.slice(0, SLOT_COUNT); diff --git a/frontend/components/Footer.vue b/frontend/components/Footer.vue index ff449547..d308eaf7 100644 --- a/frontend/components/Footer.vue +++ b/frontend/components/Footer.vue @@ -1,65 +1,141 @@ + @@ -164,25 +285,31 @@ import { useMainStore } from '@/store'; import { useI18n } from 'vue-i18n'; import changelogData from '@/data/changelog.json'; import { trackEvent } from '@/utils/analytics'; +import { docsUrl } from '@/utils/docs-url.js'; import { formatIsoDate } from '@/utils/time-utils'; import { fallbackChain } from '@/utils/locale-registry.js'; import { Sheet, SheetContent, SheetClose } from '@/components/ui/sheet'; -import { JnTooltip } from '@/components/ui/tooltip'; +import { Dialog, DialogContent, DialogHeader } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; -import { Compass, ExternalLink, CircleFadingArrowUp, CirclePlus, BugOff, Smile } from '@lucide/vue'; +import { Compass, ExternalLink, CircleFadingArrowUp, CirclePlus, BugOff, Smile, Terminal, Feather } from '@lucide/vue'; +import brandIcon from './svgicons/Brand.vue'; import { Icon } from '@iconify/vue'; +import { useRouteActive } from '@/composables/use-route-active.js'; const { t, tm, rt, locale } = useI18n(); const store = useMainStore(); -const isMobile = computed(() => store.isMobile); const configs = computed(() => store.configs); - +const curl = computed(() => store.curl); +const curlDomainsHadSet = computed(() => store.curlDomainsHadSet); const tabs = ['about', 'changelog', 'acknowledgement']; const content = ref('about'); + +const headingClass = 'mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground'; +const linkClass = 'inline-flex items-center gap-1 text-sm text-muted-foreground no-underline transition-colors hover:text-foreground cursor-pointer'; // Static data from JSON — reverse once via computed so the template stays tidy. const changelogReversed = computed(() => changelogData.slice().reverse()); @@ -240,8 +367,10 @@ const changelogBadgeClass = (type) => { return ''; }; -// Sheet toggle and store.openSheet bidirectional binding -const isOpen = computed(() => store.openSheet === 'about'); +// Sheet toggle and store.openSheet bidirectional binding. Home and ToolPage are +// both cached with a Footer each; only the page on screen opens its sheet. +const routeActive = useRouteActive(); +const isOpen = computed(() => routeActive.value && store.openSheet === 'about'); const onOpenChange = (val) => { store.setOpenSheet(val ? 'about' : null); }; @@ -261,6 +390,12 @@ const openAboutTab = (tab, trackLabel) => { trackEvent('Footer', 'FooterClick', trackLabel); }; +const isCurlOpen = ref(false); +const openCurl = () => { + isCurlOpen.value = true; + trackEvent('Footer', 'FooterClick', 'Curl'); +}; + watch(content, () => { nextTick(() => { if (sheetBody.value) sheetBody.value.scrollTop = 0; @@ -268,3 +403,20 @@ watch(content, () => { }); + + diff --git a/frontend/components/Home.vue b/frontend/components/Home.vue index 43c188ca..e608e91e 100644 --- a/frontend/components/Home.vue +++ b/frontend/components/Home.vue @@ -1,8 +1,5 @@ diff --git a/frontend/components/PageBreadcrumb.vue b/frontend/components/PageBreadcrumb.vue new file mode 100644 index 00000000..f9b8625c --- /dev/null +++ b/frontend/components/PageBreadcrumb.vue @@ -0,0 +1,64 @@ + + + diff --git a/frontend/components/PrivacyPolicy.vue b/frontend/components/PrivacyPolicy.vue index ebb6b6d3..6bbf808d 100644 --- a/frontend/components/PrivacyPolicy.vue +++ b/frontend/components/PrivacyPolicy.vue @@ -1,17 +1,15 @@