mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-29 16:15:04 +00:00
Merged master
This commit is contained in:
34
.dockerignore
Normal file
34
.dockerignore
Normal file
@@ -0,0 +1,34 @@
|
||||
**/*.log
|
||||
**/*.md
|
||||
**/*.php~
|
||||
**/*.dist.php
|
||||
**/*.dist
|
||||
**/*.cache
|
||||
**/._*
|
||||
**/.dockerignore
|
||||
**/.DS_Store
|
||||
**/.git/
|
||||
**/.gitattributes
|
||||
**/.gitignore
|
||||
**/.gitmodules
|
||||
**/compose.*.yaml
|
||||
**/compose.*.yml
|
||||
**/compose.yaml
|
||||
**/compose.yml
|
||||
**/docker-compose.*.yaml
|
||||
**/docker-compose.*.yml
|
||||
**/docker-compose.yaml
|
||||
**/docker-compose.yml
|
||||
**/Dockerfile
|
||||
**/Thumbs.db
|
||||
.github/
|
||||
docs/
|
||||
public/bundles/
|
||||
tests/
|
||||
var/
|
||||
vendor/
|
||||
.editorconfig
|
||||
.env.*.local
|
||||
.env.local
|
||||
.env.local.php
|
||||
.env.test
|
||||
36
.env
36
.env
@@ -16,7 +16,7 @@
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_SECRET=9bf13abc0017f7656f631c6ca2510e02
|
||||
APP_SECRET=cacb7ba341ce4afca66611c4956a4699
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
@@ -29,37 +29,38 @@ APP_SECRET=9bf13abc0017f7656f631c6ca2510e02
|
||||
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> symfony/messenger ###
|
||||
# Choose one of the transports below
|
||||
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
||||
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
||||
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||
###< symfony/messenger ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
# MAILER_DSN=null://null
|
||||
###< symfony/mailer ###
|
||||
###> lexik/jwt-authentication-bundle ###
|
||||
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||
JWT_PASSPHRASE=0456f23bb41aa797092f1422dc9295e9855c3518fa82969a10716bf09f99d24d
|
||||
###< lexik/jwt-authentication-bundle ###
|
||||
|
||||
###> nelmio/cors-bundle ###
|
||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||
###< nelmio/cors-bundle ###
|
||||
|
||||
###> lexik/jwt-authentication-bundle ###
|
||||
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||
JWT_PASSPHRASE=827c9f8cce8bb82e75b2aec4a14a61f572ac28c7a8531f08dcdf1652573a7049
|
||||
###< lexik/jwt-authentication-bundle ###
|
||||
|
||||
###> symfony/lock ###
|
||||
# Choose one of the stores below
|
||||
# postgresql+advisory://db_user:db_password@localhost/db_name
|
||||
LOCK_DSN=flock
|
||||
###< symfony/lock ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
MAILER_DSN=null://null
|
||||
###< symfony/mailer ###
|
||||
|
||||
###> symfony/messenger ###
|
||||
# Choose one of the transports below
|
||||
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
||||
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
||||
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||
###< symfony/messenger ###
|
||||
|
||||
|
||||
MAILER_SENDER_NAME="Domain Watchdog"
|
||||
MAILER_SENDER_EMAIL=notifications@example.com
|
||||
REGISTRATION_ENABLED=true
|
||||
REGISTRATION_VERIFY_EMAIL=false
|
||||
OAUTH_CLIENT_ID=
|
||||
OAUTH_CLIENT_SECRET=
|
||||
OAUTH_AUTHORIZATION_URL=
|
||||
@@ -75,3 +76,4 @@ OUTGOING_IP=
|
||||
LIMITED_FEATURES=false
|
||||
LIMIT_MAX_WATCHLIST=0
|
||||
LIMIT_MAX_WATCHLIST_DOMAINS=0
|
||||
LIMIT_MAX_WATCHLIST_WEBHOOKS=0
|
||||
|
||||
139
.github/workflows/publish.yml
vendored
Normal file
139
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
attestations: write
|
||||
id-token: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push Docker images
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
target: frankenphp_prod
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ github.repository }},name-canonical=true,push=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
attestations: write
|
||||
id-token: write
|
||||
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ github.repository }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ github.repository }}:${{ steps.meta.outputs.version }}
|
||||
2
.github/workflows/symfony.yml
vendored
2
.github/workflows/symfony.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: mbstring, xml, intl, curl, iconv, pdo_pgsql, sodium, zip
|
||||
extensions: mbstring, xml, intl, curl, iconv, pdo_pgsql, sodium, zip, http
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --prefer-dist --no-progress --no-suggest --optimize-autoloader
|
||||
|
||||
@@ -13,14 +13,12 @@ identifiers:
|
||||
- type: url
|
||||
value: >-
|
||||
https://github.com/maelgangloff/domain-watcher/releases
|
||||
description: Release of scolengo-api
|
||||
description: Release of Domain Watchdog
|
||||
repository-code: 'https://github.com/maelgangloff/domain-watchdog'
|
||||
abstract: Unofficial Node.js API client of Skolengo EMS.
|
||||
abstract: An app that uses RDAP to collect publicly available info about domains, track their history, and purchase them
|
||||
keywords:
|
||||
- DOMAIN
|
||||
- RDAP
|
||||
- WHOIS
|
||||
license: AGPL-3.0-or-later
|
||||
version: 0.0.1
|
||||
date-released: '2024-07-11'
|
||||
license-url: 'https://github.com/maelgangloff/domain-watchdog/blob/master/LICENSE'
|
||||
license-url: 'https://github.com/maelgangloff/domain-watchdog/blob/master/LICENSE'
|
||||
|
||||
103
Dockerfile
Normal file
103
Dockerfile
Normal file
@@ -0,0 +1,103 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
|
||||
# Versions
|
||||
FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream
|
||||
|
||||
# The different stages of this Dockerfile are meant to be built into separate images
|
||||
# https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage
|
||||
# https://docs.docker.com/compose/compose-file/#target
|
||||
|
||||
# Base FrankenPHP image
|
||||
FROM frankenphp_upstream AS frankenphp_base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
VOLUME /app/var/
|
||||
|
||||
# persistent / runtime deps
|
||||
# hadolint ignore=DL3008
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
acl \
|
||||
file \
|
||||
gettext \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN set -eux; \
|
||||
install-php-extensions \
|
||||
@composer \
|
||||
apcu \
|
||||
intl \
|
||||
opcache \
|
||||
zip
|
||||
|
||||
RUN set -eux; \
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -; \
|
||||
apt-get install -y nodejs; \
|
||||
npm install -g yarn
|
||||
|
||||
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
ENV PHP_INI_SCAN_DIR=":$PHP_INI_DIR/app.conf.d"
|
||||
|
||||
###> recipes ###
|
||||
###> doctrine/doctrine-bundle ###
|
||||
RUN install-php-extensions pdo_pgsql
|
||||
###< doctrine/doctrine-bundle ###
|
||||
###< recipes ###
|
||||
|
||||
COPY --link frankenphp/conf.d/10-app.ini $PHP_INI_DIR/app.conf.d/
|
||||
COPY --link --chmod=755 frankenphp/docker-entrypoint.sh /usr/local/bin/docker-entrypoint
|
||||
COPY --link frankenphp/Caddyfile /etc/caddy/Caddyfile
|
||||
|
||||
ENTRYPOINT ["docker-entrypoint"]
|
||||
HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1
|
||||
CMD [ "frankenphp", "run", "--config", "/etc/caddy/Caddyfile" ]
|
||||
|
||||
# Dev FrankenPHP image
|
||||
FROM frankenphp_base AS frankenphp_dev
|
||||
|
||||
ENV APP_ENV=dev XDEBUG_MODE=off
|
||||
|
||||
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
RUN set -eux; \
|
||||
install-php-extensions \
|
||||
xdebug
|
||||
|
||||
COPY --link frankenphp/conf.d/20-app.dev.ini $PHP_INI_DIR/app.conf.d/
|
||||
|
||||
CMD [ "frankenphp", "run", "--config", "/etc/caddy/Caddyfile", "--watch" ]
|
||||
|
||||
# Prod FrankenPHP image
|
||||
FROM frankenphp_base AS frankenphp_prod
|
||||
|
||||
ENV APP_ENV=prod
|
||||
ENV FRANKENPHP_CONFIG="import worker.Caddyfile"
|
||||
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
COPY --link frankenphp/conf.d/20-app.prod.ini $PHP_INI_DIR/app.conf.d/
|
||||
COPY --link frankenphp/worker.Caddyfile /etc/caddy/worker.Caddyfile
|
||||
|
||||
# prevent the reinstallation of vendors at every changes in the source code
|
||||
COPY --link composer.* symfony.* ./
|
||||
RUN set -eux; \
|
||||
install-php-extensions redis; \
|
||||
composer install --no-cache --prefer-dist --no-dev --no-autoloader --no-scripts --no-progress
|
||||
|
||||
# copy sources
|
||||
COPY --link . ./
|
||||
RUN rm -Rf frankenphp/
|
||||
|
||||
RUN set -eux; \
|
||||
mkdir -p var/cache var/log; \
|
||||
composer dump-autoload --classmap-authoritative --no-dev; \
|
||||
composer dump-env prod; \
|
||||
composer run-script --no-dev post-install-cmd; \
|
||||
chmod +x bin/console; \
|
||||
php bin/console assets:install; \
|
||||
yarn install; \
|
||||
yarn run build; \
|
||||
yarn run ttag:po2json; \
|
||||
rm -rf node_modules; \
|
||||
sync
|
||||
20
INSTALL.md
20
INSTALL.md
@@ -8,7 +8,7 @@ on [How to deploy a Symfony application](https://symfony.com/doc/current/deploym
|
||||
### Prerequisites
|
||||
|
||||
- PHP 8.2 or higher
|
||||
- PostgreSQL
|
||||
- PostgreSQL 16 or higher
|
||||
|
||||
In order to retrieve information about domain names, Domain Watchdog will query the RDAP server responsible for the TLD.
|
||||
It is crucial that the Domain Watchdog instance is placed in a clean environment from which these servers can be
|
||||
@@ -45,7 +45,11 @@ git clone https://github.com/maelgangloff/domain-watchdog.git
|
||||
```shell
|
||||
symfony server:start
|
||||
```
|
||||
6. Don't forget to set up workers to process the [message queue](https://symfony.com/doc/current/messenger.html)
|
||||
6. Build assets:
|
||||
```shell
|
||||
php bin/console assets:install
|
||||
```
|
||||
7. Don't forget to set up workers to process the [message queue](https://symfony.com/doc/current/messenger.html)
|
||||
|
||||
#### Frontend
|
||||
|
||||
@@ -63,10 +67,10 @@ git clone https://github.com/maelgangloff/domain-watchdog.git
|
||||
```
|
||||
4. Add and modify the following files as you wish:
|
||||
~~~
|
||||
public/contents/home.md
|
||||
public/contents/privacy.md
|
||||
public/contents/tos.md
|
||||
public/contents/faq.md
|
||||
public/content/home.md
|
||||
public/content/privacy.md
|
||||
public/content/tos.md
|
||||
public/content/faq.md
|
||||
public/images/icons-512.png
|
||||
public/images/banner.png
|
||||
public/favicon.ico
|
||||
@@ -96,6 +100,10 @@ git pull origin master
|
||||
```shell
|
||||
php bin/console cache:clear
|
||||
```
|
||||
4. Build assets:
|
||||
```shell
|
||||
php bin/console assets:install
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
|
||||
91
README.md
91
README.md
@@ -1,7 +1,10 @@
|
||||
# Domain Watchdog
|
||||
<p align="center"><img src="https://github.com/user-attachments/assets/942ddfd0-2c76-4b00-bd9f-727cfddc0103" alt="Domain Watchdog" width="150" height="150" /></p>
|
||||
<h1 align="center"><b>Domain Watchdog</b></h1>
|
||||
<p align="center">Your companion in the quest for domain names 🔍 <br/><a href="https://domainwatchdog.eu">domainwatchdog.eu »</a></p>
|
||||
<br/>
|
||||
|
||||
Domain Watchdog is a standalone application that utilizes RDAP to gather publicly accessible information about domain
|
||||
names, track their history, and automatically purchase them. For more information please check [the wiki](https://github.com/maelgangloff/domain-watchdog/wiki) !
|
||||
Domain Watchdog is an app that uses RDAP to collect publicly available info about domains, track their history, and purchase them.
|
||||
For more information please check [the wiki](https://github.com/maelgangloff/domain-watchdog/wiki) !
|
||||
|
||||
## Why use it?
|
||||
|
||||
@@ -14,20 +17,30 @@ Although the RDAP and WHOIS protocols allow you to obtain precise information ab
|
||||
perform a reverse search to discover a list of domain names associated with an entity. Additionally, accessing a
|
||||
detailed history of events (ownership changes, renewals, etc.) is not feasible with these protocols.
|
||||
|
||||
## How it works?
|
||||
## Install
|
||||
|
||||
### RDAP search
|
||||
> [!TIP]
|
||||
> For more details on the installation procedure, please refer to [INSTALL.md](/INSTALL.md).
|
||||
|
||||
The latest version of the WHOIS protocol was standardized in 2004 by RFC 3912.[^1] This protocol allows anyone to
|
||||
retrieve key information concerning a domain name, an IP address, or an entity registered with a registry.
|
||||
### Docker Deployment
|
||||
|
||||
ICANN launched a global vote in 2023 to propose replacing the WHOIS protocol with RDAP. As a result, registries and
|
||||
registrars will no longer be required to support WHOIS from 2025 (*WHOIS Sunset Date*).[^2]
|
||||
1. Clone the repository
|
||||
2. Modify environment variables (.env) and add static files to customize your instance (see [INSTALL.md](/INSTALL.md))
|
||||
3. Pull the latest version of the Domain Watchdog image from Docker Hub.
|
||||
```shell
|
||||
docker compose pull
|
||||
```
|
||||
4. Start the project in production environment. If you want, you can also build the Docker image to use yourself.
|
||||
```shell
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Domain Watchdog uses the RDAP protocol, which will soon be the new standard for retrieving information concerning domain
|
||||
names. The data is organized in a SQL database to minimize space by ensuring an entity is not repeated.
|
||||
By default, the container listens on http://localhost:8080, but you can configure this in environment variables.
|
||||
See the [Docker Compose file](./docker-compose.yml).
|
||||
|
||||
### Connector Provider
|
||||
## Features
|
||||
|
||||
### Auto-purchase domain
|
||||
|
||||
A connector is a way to order a domain name. It is important to mention that this project does not act as a payment
|
||||
intermediary.
|
||||
@@ -42,17 +55,45 @@ The table below lists the supported API connector providers:
|
||||
| GANDI | https://api.gandi.net/docs/domains/ | **Yes** |
|
||||
| NAMECHEAP | https://www.namecheap.com/support/api/methods/domains/create/ | |
|
||||
|
||||
### Watchlist
|
||||
|
||||
A watchlist is a list of domain names, triggers and possibly an API connector.
|
||||
They allow you to follow the life of the listed domain names and send you a notification when a change has been
|
||||
detected.
|
||||
|
||||
If a domain has expired and a connector is linked to the Watchlist, then Domain Watchdog will try to order it via the
|
||||
connector provider's API.
|
||||
|
||||
Note: If the same domain name is present on several Watchlists, on the same principle as the raise condition, it is not
|
||||
possible to predict in advance which user will win the domain name. The choice is left to chance.
|
||||
Note: If the same domain name is present on several Watchlists, it is not possible to predict in advance which user will
|
||||
win the domain name. The choice is left to chance.
|
||||
|
||||
### Monitoring
|
||||
|
||||

|
||||
|
||||
|
||||
A watchlist is a list of domain names, triggers and possibly an API connector.
|
||||
|
||||
They allow you to follow the life of the listed domain names and send you a notification when a change has been
|
||||
detected.
|
||||
|
||||
A notification to the user is sent when a new event occurs on one of the domain names in the Watchlist. This can be an
|
||||
email or a chat via Webhook (Slack, Mattermost, Discord, ...). An iCalendar export of domain events is possible.
|
||||
|
||||
### RDAP search
|
||||
|
||||
The latest version of the WHOIS protocol was standardized in 2004 by RFC 3912.[^1] This protocol allows anyone to
|
||||
retrieve key information concerning a domain name, an IP address, or an entity registered with a registry.
|
||||
|
||||
ICANN launched a global vote in 2023 to propose replacing the WHOIS protocol with RDAP. As a result, registries and
|
||||
registrars will no longer be required to support WHOIS from 2025 (*WHOIS Sunset Date*).[^2]
|
||||
|
||||
Domain Watchdog uses the RDAP protocol, which will soon be the new standard for retrieving information concerning domain
|
||||
names.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
> [!IMPORTANT]
|
||||
> * Domain Watchdog is an opensource project distributed under *GNU Affero General Public License v3.0 or later* license
|
||||
> * In the internal operation, everything is done to perform the least possible RDAP requests: rate limit, intelligent
|
||||
caching system, etc.
|
||||
> * Please note that this project is NOT affiliated IN ANY WAY with the API Providers used to order domain names.
|
||||
> * The project installers are responsible for the use of their own instance.
|
||||
> * Under no circumstances will the owner of this project be held responsible for other cases over which he has no control.
|
||||
|
||||
## Useful documentation
|
||||
|
||||
@@ -61,16 +102,6 @@ possible to predict in advance which user will win the domain name. The choice i
|
||||
> - [RFC 7483 : JSON Responses for the Registration Data Access Protocol (RDAP)](https://datatracker.ietf.org/doc/html/rfc7483)
|
||||
> - [RFC 7484 : Finding the Authoritative Registration Data (RDAP) Service](https://datatracker.ietf.org/doc/html/rfc7484)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
> [!WARNING]
|
||||
> * Domain Watchdog is an opensource project distributed under *GNU Affero General Public License v3.0 or later* license
|
||||
> * In the internal opration, everything is done to perform the least possible RDAP requests: rate limit, intelligent
|
||||
caching system, etc.
|
||||
> * Please note that this project is NOT affiliated IN ANY WAY with the API Providers used to order domain names.
|
||||
> * The project installers are responsible for the use of their own instance.
|
||||
> * In no event the owner of this project will not be held responsible for other instances over which he has no control.
|
||||
|
||||
## Licensing
|
||||
|
||||
This source code of this project is licensed under *GNU Affero General Public License v3.0 or later*.
|
||||
|
||||
@@ -4,10 +4,10 @@ import TextPage from "./pages/TextPage";
|
||||
import DomainSearchPage from "./pages/search/DomainSearchPage";
|
||||
import EntitySearchPage from "./pages/search/EntitySearchPage";
|
||||
import NameserverSearchPage from "./pages/search/NameserverSearchPage";
|
||||
import TldPage from "./pages/info/TldPage";
|
||||
import StatisticsPage from "./pages/info/StatisticsPage";
|
||||
import TldPage from "./pages/search/TldPage";
|
||||
import StatisticsPage from "./pages/StatisticsPage";
|
||||
import WatchlistPage from "./pages/tracking/WatchlistPage";
|
||||
import UserPage from "./pages/watchdog/UserPage";
|
||||
import UserPage from "./pages/UserPage";
|
||||
import React, {useCallback, useEffect, useMemo, useState} from "react";
|
||||
import {getUser} from "./utils/api";
|
||||
import LoginPage, {AuthenticatedContext} from "./pages/LoginPage";
|
||||
@@ -40,7 +40,8 @@ export default function App() {
|
||||
const contextValue = useMemo(() => ({
|
||||
authenticated,
|
||||
setIsAuthenticated
|
||||
}), [authenticated, setIsAuthenticated]);
|
||||
}), [authenticated, setIsAuthenticated])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
getUser().then(() => {
|
||||
@@ -77,13 +78,12 @@ export default function App() {
|
||||
<Route path="/search/domain" element={<DomainSearchPage/>}/>
|
||||
<Route path="/search/entity" element={<EntitySearchPage/>}/>
|
||||
<Route path="/search/nameserver" element={<NameserverSearchPage/>}/>
|
||||
|
||||
<Route path="/info/tld" element={<TldPage/>}/>
|
||||
<Route path="/info/stats" element={<StatisticsPage/>}/>
|
||||
<Route path="/search/tld" element={<TldPage/>}/>
|
||||
|
||||
<Route path="/tracking/watchlist" element={<WatchlistPage/>}/>
|
||||
<Route path="/tracking/connectors" element={<ConnectorsPage/>}/>
|
||||
|
||||
<Route path="/stats" element={<StatisticsPage/>}/>
|
||||
<Route path="/user" element={<UserPage/>}/>
|
||||
|
||||
<Route path="/faq" element={<TextPage resource='faq.md'/>}/>
|
||||
@@ -97,7 +97,7 @@ export default function App() {
|
||||
</div>
|
||||
</Layout.Content>
|
||||
<Layout.Footer style={{textAlign: 'center'}}>
|
||||
<Space size='middle'>
|
||||
<Space size='middle' wrap align='center'>
|
||||
<Link to='/tos'><Button type='text'>{t`TOS`}</Button></Link>
|
||||
<Link to='/privacy'><Button type='text'>{t`Privacy Policy`}</Button></Link>
|
||||
<Link to='/faq'><Button type='text'>{t`FAQ`}</Button></Link>
|
||||
|
||||
@@ -4,7 +4,8 @@ import React, {useContext, useEffect} from "react";
|
||||
import {getUser, login} from "../utils/api";
|
||||
import {AuthenticatedContext} from "../pages/LoginPage";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {showErrorAPI} from "../utils";
|
||||
|
||||
import {showErrorAPI} from "../utils/functions/showErrorAPI";
|
||||
|
||||
|
||||
type FieldType = {
|
||||
|
||||
@@ -3,7 +3,8 @@ import {t} from "ttag";
|
||||
import React, {useState} from "react";
|
||||
import {register} from "../utils/api";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {showErrorAPI} from "../utils";
|
||||
|
||||
import {showErrorAPI} from "../utils/functions/showErrorAPI";
|
||||
|
||||
|
||||
type FieldType = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ItemType, MenuItemType} from "antd/lib/menu/interface";
|
||||
import {ItemType} from "antd/lib/menu/interface";
|
||||
import {t} from "ttag";
|
||||
import {
|
||||
AimOutlined,
|
||||
@@ -22,8 +22,7 @@ import {useNavigate} from "react-router-dom";
|
||||
export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
|
||||
const menuItems: ItemType<MenuItemType>[] = [
|
||||
const menuItems: ItemType[] = [
|
||||
{
|
||||
key: 'home',
|
||||
label: t`Home`,
|
||||
@@ -49,7 +48,7 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
|
||||
label: t`TLD`,
|
||||
title: t`TLD list`,
|
||||
disabled: !isAuthenticated,
|
||||
onClick: () => navigate('/info/tld')
|
||||
onClick: () => navigate('/search/tld')
|
||||
},
|
||||
{
|
||||
key: 'entity-finder',
|
||||
@@ -64,7 +63,8 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
|
||||
icon: <CloudServerOutlined/>,
|
||||
label: t`Nameserver`,
|
||||
title: t`Nameserver Finder`,
|
||||
disabled: true
|
||||
disabled: true,
|
||||
onClick: () => navigate('/search/nameserver')
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -93,8 +93,8 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
|
||||
key: 'stats',
|
||||
icon: <LineChartOutlined/>,
|
||||
label: t`Statistics`,
|
||||
disabled: true,
|
||||
onClick: () => navigate('/info/stats')
|
||||
disabled: !isAuthenticated,
|
||||
onClick: () => navigate('/stats')
|
||||
}
|
||||
]
|
||||
|
||||
@@ -103,7 +103,6 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
|
||||
key: 'account',
|
||||
icon: <UserOutlined/>,
|
||||
label: t`My Account`,
|
||||
disabled: !isAuthenticated,
|
||||
onClick: () => navigate('/user')
|
||||
}, {
|
||||
key: 'logout',
|
||||
@@ -122,7 +121,6 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
|
||||
}
|
||||
|
||||
return <Menu
|
||||
defaultSelectedKeys={['home']}
|
||||
defaultOpenKeys={['search', 'info', 'tracking', 'doc']}
|
||||
mode="inline"
|
||||
theme="dark"
|
||||
|
||||
47
assets/components/search/DomainDiagram.tsx
Normal file
47
assets/components/search/DomainDiagram.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React, {useEffect} from "react";
|
||||
import {Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState} from "@xyflow/react";
|
||||
import {Flex} from "antd";
|
||||
import {Domain} from "../../utils/api";
|
||||
import {getLayoutedElements} from "../tracking/watchlist/diagram/getLayoutedElements";
|
||||
import {domainEntitiesToNode, domainToNode, nsToNode, tldToNode} from "../tracking/watchlist/diagram/watchlistToNodes";
|
||||
import {domainEntitiesToEdges, domainNSToEdges, tldToEdge} from "../tracking/watchlist/diagram/watchlistToEdges";
|
||||
|
||||
export function DomainDiagram({domain}: { domain: Domain }) {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const e = getLayoutedElements([
|
||||
domainToNode(domain),
|
||||
...domainEntitiesToNode(domain, true),
|
||||
tldToNode(domain.tld),
|
||||
...domain.nameservers.map(nsToNode)
|
||||
].flat(), [
|
||||
domainEntitiesToEdges(domain, true),
|
||||
tldToEdge(domain),
|
||||
...domainNSToEdges(domain)
|
||||
].flat())
|
||||
|
||||
setNodes(e.nodes)
|
||||
setEdges(e.edges)
|
||||
}, [])
|
||||
|
||||
return <Flex style={{width: '100%', height: '100vh'}}>
|
||||
<ReactFlow
|
||||
fitView
|
||||
colorMode='dark'
|
||||
nodesConnectable={false}
|
||||
edgesReconnectable={false}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
style={{width: '100%', height: '100%'}}
|
||||
>
|
||||
<MiniMap/>
|
||||
<Controls/>
|
||||
<Background/>
|
||||
</ReactFlow>
|
||||
</Flex>
|
||||
}
|
||||
45
assets/components/search/DomainLifecycleSteps.tsx
Normal file
45
assets/components/search/DomainLifecycleSteps.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import {StepProps, Steps, Tooltip} from "antd";
|
||||
import React from "react";
|
||||
import {t} from "ttag";
|
||||
import {CheckOutlined, DeleteOutlined, ExclamationCircleOutlined, SignatureOutlined} from "@ant-design/icons";
|
||||
import {rdapEventDetailTranslation, rdapStatusCodeDetailTranslation} from "../../utils/functions/rdapTranslation";
|
||||
|
||||
export function DomainLifecycleSteps({status}: { status: string[] }) {
|
||||
|
||||
const rdapEventDetailTranslated = rdapEventDetailTranslation()
|
||||
const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation()
|
||||
|
||||
|
||||
const steps: StepProps[] = [
|
||||
{
|
||||
title: <Tooltip title={rdapEventDetailTranslated.registration}>{t`Registration`}</Tooltip>,
|
||||
icon: <SignatureOutlined style={{color: 'green'}}/>
|
||||
},
|
||||
{
|
||||
title: <Tooltip title={rdapStatusCodeDetailTranslated.active}>{t`Active`}</Tooltip>,
|
||||
icon: <CheckOutlined/>
|
||||
},
|
||||
{
|
||||
title: <Tooltip
|
||||
title={rdapStatusCodeDetailTranslated["redemption period"]}>{t`Redemption Period`}</Tooltip>,
|
||||
icon: <ExclamationCircleOutlined style={{color: 'orangered'}}/>
|
||||
},
|
||||
{
|
||||
title: <Tooltip title={rdapStatusCodeDetailTranslated["pending delete"]}>{t`Pending Delete`}</Tooltip>,
|
||||
icon: <DeleteOutlined style={{color: 'palevioletred'}}/>
|
||||
}
|
||||
]
|
||||
|
||||
let currentStep = 1
|
||||
|
||||
if (status.includes('redemption period')) {
|
||||
currentStep = 2
|
||||
} else if (status.includes('pending delete')) {
|
||||
currentStep = 3
|
||||
}
|
||||
|
||||
return <Steps
|
||||
current={currentStep}
|
||||
items={steps}
|
||||
/>
|
||||
}
|
||||
76
assets/components/search/DomainResult.tsx
Normal file
76
assets/components/search/DomainResult.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import {Badge, Card, Divider, Flex, Space, Tag, Tooltip, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {EventTimeline} from "./EventTimeline";
|
||||
import {EntitiesList} from "./EntitiesList";
|
||||
import {DomainDiagram} from "./DomainDiagram";
|
||||
import React from "react";
|
||||
import {Domain} from "../../utils/api";
|
||||
import {rdapStatusCodeDetailTranslation} from "../../utils/functions/rdapTranslation";
|
||||
import {regionNames} from "../../i18n";
|
||||
|
||||
import {getCountryCode} from "../../utils/functions/getCountryCode";
|
||||
import {eppStatusCodeToColor} from "../../utils/functions/eppStatusCodeToColor";
|
||||
import {DomainLifecycleSteps} from "./DomainLifecycleSteps";
|
||||
|
||||
export function DomainResult({domain}: { domain: Domain }) {
|
||||
|
||||
const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation()
|
||||
const {tld, events} = domain
|
||||
const domainEvents = events.sort((e1, e2) => new Date(e2.date).getTime() - new Date(e1.date).getTime())
|
||||
|
||||
return <Space direction="vertical" size="middle" style={{width: '100%'}}>
|
||||
|
||||
<Badge.Ribbon text={
|
||||
<Tooltip
|
||||
title={tld.type === 'ccTLD' ? regionNames.of(getCountryCode(tld.tld)) : tld.type === 'gTLD' ? tld?.registryOperator : undefined}>
|
||||
{`.${domain.tld.tld.toUpperCase()} (${tld.type})`}
|
||||
</Tooltip>
|
||||
}
|
||||
color={
|
||||
tld.type === 'ccTLD' ? 'purple' :
|
||||
(tld.type === 'gTLD' && tld.specification13) ? "volcano" :
|
||||
tld.type === 'gTLD' ? "green"
|
||||
: "cyan"
|
||||
}>
|
||||
|
||||
<Card title={<Space>
|
||||
{domain.ldhName}{domain.handle && <Typography.Text code>{domain.handle}</Typography.Text>}
|
||||
</Space>}
|
||||
size="small">
|
||||
{
|
||||
domain.events.length > 0 && <DomainLifecycleSteps status={domain.status}/>
|
||||
}
|
||||
{domain.status.length > 0 &&
|
||||
<>
|
||||
<Divider orientation="left">{t`EPP Status Codes`}</Divider>
|
||||
<Flex gap="4px 0" wrap>
|
||||
{
|
||||
domain.status.map(s =>
|
||||
<Tooltip
|
||||
placement='bottomLeft'
|
||||
title={s in rdapStatusCodeDetailTranslated ? rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] : undefined}>
|
||||
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</Flex>
|
||||
</>
|
||||
}
|
||||
{
|
||||
domain.events.length > 0 && <>
|
||||
<Divider orientation="left">{t`Timeline`}</Divider>
|
||||
<EventTimeline events={domainEvents}/>
|
||||
</>
|
||||
}
|
||||
{
|
||||
domain.entities.length > 0 &&
|
||||
<>
|
||||
<Divider orientation="left">{t`Entities`}</Divider>
|
||||
<EntitiesList domain={domain}/>
|
||||
</>
|
||||
}
|
||||
</Card>
|
||||
</Badge.Ribbon>
|
||||
<DomainDiagram domain={domain}/>
|
||||
</Space>
|
||||
}
|
||||
@@ -8,13 +8,10 @@ export type FieldType = {
|
||||
}
|
||||
|
||||
export function DomainSearchBar({onFinish}: { onFinish: (values: FieldType) => void }) {
|
||||
|
||||
return <Form
|
||||
name="basic"
|
||||
labelCol={{span: 8}}
|
||||
wrapperCol={{span: 16}}
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
style={{width: '100%'}}
|
||||
>
|
||||
<Form.Item<FieldType>
|
||||
name="ldhName"
|
||||
@@ -28,8 +25,13 @@ export function DomainSearchBar({onFinish}: { onFinish: (values: FieldType) => v
|
||||
min: 2
|
||||
}]}
|
||||
>
|
||||
<Input size="large" prefix={<SearchOutlined/>} placeholder="example.com" autoFocus={true}
|
||||
autoComplete='off'/>
|
||||
<Input style={{textAlign: 'center'}}
|
||||
size="large"
|
||||
prefix={<SearchOutlined/>}
|
||||
placeholder="example.com"
|
||||
autoComplete='off'
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
}
|
||||
@@ -1,53 +1,36 @@
|
||||
import vCard from "vcf";
|
||||
import {Avatar, List} from "antd";
|
||||
import {BankOutlined, IdcardOutlined, SignatureOutlined, ToolOutlined, UserOutlined} from "@ant-design/icons";
|
||||
import {List, Tag, Tooltip} from "antd";
|
||||
import React from "react";
|
||||
import {Domain} from "../../utils/api";
|
||||
import {t} from "ttag";
|
||||
import {rdapRoleDetailTranslation, rdapRoleTranslation} from "../../utils/functions/rdapTranslation";
|
||||
import {roleToAvatar} from "../../utils/functions/roleToAvatar";
|
||||
import {rolesToColor} from "../../utils/functions/rolesToColor";
|
||||
import {entityToName} from "../../utils/functions/entityToName";
|
||||
import {sortDomainEntities} from "../../utils/functions/sortDomainEntities";
|
||||
|
||||
|
||||
export function EntitiesList({domain}: { domain: Domain }) {
|
||||
const domainRole = {
|
||||
registrant: t`Registrant`,
|
||||
technical: t`Technical`,
|
||||
administrative: t`Administrative`,
|
||||
abuse: t`Abuse`,
|
||||
billing: t`Billing`,
|
||||
registrar: t`Registrar`,
|
||||
reseller: t`Reseller`,
|
||||
sponsor: t`Sponsor`,
|
||||
proxy: t`Proxy`,
|
||||
notifications: t`Notifications`,
|
||||
noc: t`Noc`
|
||||
}
|
||||
const rdapRoleTranslated = rdapRoleTranslation()
|
||||
const rdapRoleDetailTranslated = rdapRoleDetailTranslation()
|
||||
|
||||
const roleToTag = (r: string) => <Tooltip
|
||||
title={r in rdapRoleDetailTranslated ? rdapRoleDetailTranslated[r as keyof typeof rdapRoleDetailTranslated] : undefined}>
|
||||
<Tag
|
||||
color={rolesToColor([r])}>{rdapRoleTranslated[r as keyof typeof rdapRoleTranslated]}</Tag>
|
||||
</Tooltip>
|
||||
|
||||
return <List
|
||||
className="demo-loadmore-list"
|
||||
itemLayout="horizontal"
|
||||
dataSource={domain.entities.sort((e1, e2) => {
|
||||
const p = (r: string[]) => r.includes('registrant') ? 4 : r.includes('administrative') ? 3 : r.includes('billing') ? 2 : 1
|
||||
return p(e2.roles) - p(e1.roles)
|
||||
})}
|
||||
renderItem={(e) => {
|
||||
const jCard = vCard.fromJSON(e.entity.jCard)
|
||||
let name = ''
|
||||
if (jCard.data.fn !== undefined && !Array.isArray(jCard.data.fn)) name = jCard.data.fn.valueOf()
|
||||
|
||||
return <List.Item>
|
||||
dataSource={sortDomainEntities(domain)}
|
||||
renderItem={(e) =>
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={<Avatar style={{backgroundColor: '#87d068'}}
|
||||
icon={e.roles.includes('registrant') ?
|
||||
<SignatureOutlined/> : e.roles.includes('registrar') ?
|
||||
<BankOutlined/> :
|
||||
e.roles.includes('technical') ?
|
||||
<ToolOutlined/> :
|
||||
e.roles.includes('administrative') ?
|
||||
<IdcardOutlined/> :
|
||||
<UserOutlined/>}/>}
|
||||
avatar={roleToAvatar(e)}
|
||||
title={e.entity.handle}
|
||||
description={name}
|
||||
description={entityToName(e)}
|
||||
/>
|
||||
<div>{e.roles.map((r) => Object.keys(domainRole).includes(r) ? domainRole[r as keyof typeof domainRole] : r).join(', ')}</div>
|
||||
{e.roles.map(roleToTag)}
|
||||
</List.Item>
|
||||
}}
|
||||
}
|
||||
/>
|
||||
}
|
||||
@@ -1,85 +1,52 @@
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
ShareAltOutlined,
|
||||
SignatureOutlined,
|
||||
SyncOutlined
|
||||
} from "@ant-design/icons";
|
||||
import {Timeline} from "antd";
|
||||
import {Timeline, Tooltip, Typography} from "antd";
|
||||
import React from "react";
|
||||
import {Domain, EventAction} from "../../utils/api";
|
||||
import {t} from "ttag";
|
||||
import {Event} from "../../utils/api";
|
||||
import useBreakpoint from "../../hooks/useBreakpoint";
|
||||
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../utils/functions/rdapTranslation";
|
||||
import {actionToColor} from "../../utils/functions/actionToColor";
|
||||
import {actionToIcon} from "../../utils/functions/actionToIcon";
|
||||
|
||||
export function actionToColor(a: EventAction) {
|
||||
return a === 'registration' ? 'green' :
|
||||
a === 'reregistration' ? 'cyan' :
|
||||
a === 'expiration' ? 'red' :
|
||||
a === 'deletion' ? 'magenta' :
|
||||
a === 'transfer' ? 'orange' :
|
||||
a === 'last changed' ? 'blue' : 'default'
|
||||
}
|
||||
|
||||
export const domainEvent = () => ({
|
||||
registration: t`Registration`,
|
||||
reregistration: t`Reregistration`,
|
||||
'last changed': t`Last changed`,
|
||||
expiration: t`Expiration`,
|
||||
deletion: t`Deletion`,
|
||||
reinstantiation: t`Reinstantiation`,
|
||||
transfer: t`Transfer`,
|
||||
locked: t`Locked`,
|
||||
unlocked: t`Unlocked`,
|
||||
'registrar expiration': t`Registrar expiration`,
|
||||
'enum validation expiration': t`ENUM validation expiration`
|
||||
})
|
||||
|
||||
export function EventTimeline({domain}: { domain: Domain }) {
|
||||
export function EventTimeline({events}: { events: Event[] }) {
|
||||
const sm = useBreakpoint('sm')
|
||||
|
||||
|
||||
const locale = navigator.language.split('-')[0]
|
||||
const domainEventTranslated = domainEvent()
|
||||
const rdapEventNameTranslated = rdapEventNameTranslation()
|
||||
const rdapEventDetailTranslated = rdapEventDetailTranslation()
|
||||
|
||||
return <Timeline
|
||||
mode={sm ? "left" : "right"}
|
||||
items={domain.events
|
||||
.sort((e1, e2) => new Date(e2.date).getTime() - new Date(e1.date).getTime())
|
||||
.map(({action, date}) => {
|
||||
let dot
|
||||
if (action === 'registration') {
|
||||
dot = <SignatureOutlined style={{fontSize: '16px'}}/>
|
||||
} else if (action === 'expiration') {
|
||||
dot = <ClockCircleOutlined style={{fontSize: '16px'}}/>
|
||||
} else if (action === 'transfer') {
|
||||
dot = <ShareAltOutlined style={{fontSize: '16px'}}/>
|
||||
} else if (action === 'last changed') {
|
||||
dot = <SyncOutlined style={{fontSize: '16px'}}/>
|
||||
} else if (action === 'deletion') {
|
||||
dot = <DeleteOutlined style={{fontSize: '16px'}}/>
|
||||
} else if (action === 'reregistration') {
|
||||
dot = <ReloadOutlined style={{fontSize: '16px'}}/>
|
||||
}
|
||||
return <>
|
||||
<Timeline
|
||||
mode={sm ? "left" : "right"}
|
||||
items={events.map(e => {
|
||||
const sameEvents = events.filter(se => se.action === e.action)
|
||||
|
||||
const eventName = Object.keys(domainEventTranslated).includes(action) ? domainEventTranslated[action as keyof typeof domainEventTranslated] : action
|
||||
const dateStr = new Date(date).toLocaleString(locale)
|
||||
const eventName = <Typography.Text style={{color: e.deleted ? 'grey' : 'default'}}>
|
||||
{e.action in rdapEventNameTranslated ? rdapEventNameTranslated[e.action as keyof typeof rdapEventNameTranslated] : e.action}
|
||||
</Typography.Text>
|
||||
|
||||
const dateStr = <Typography.Text
|
||||
style={{color: e.deleted ? 'grey' : 'default'}}>{new Date(e.date).toLocaleString(locale)}
|
||||
</Typography.Text>
|
||||
|
||||
const eventDetail = e.action in rdapEventDetailTranslated ? rdapEventDetailTranslated[e.action as keyof typeof rdapEventDetailTranslated] : undefined
|
||||
|
||||
const text = sm ? {
|
||||
children: <>{eventName} {dateStr}</>
|
||||
children: <Tooltip placement='bottom' title={eventDetail}>
|
||||
{eventName} {dateStr}
|
||||
</Tooltip>
|
||||
} : {
|
||||
label: dateStr,
|
||||
children: eventName,
|
||||
children: <Tooltip placement='left' title={eventDetail}>{eventName}</Tooltip>,
|
||||
}
|
||||
|
||||
return {
|
||||
color: actionToColor(action),
|
||||
dot,
|
||||
pending: new Date(date).getTime() > new Date().getTime(),
|
||||
color: e.deleted ? 'grey' : actionToColor(e.action),
|
||||
dot: actionToIcon(e.action),
|
||||
pending: new Date(e.date).getTime() > new Date().getTime(),
|
||||
...text
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import {Card, Divider, Popconfirm, Space, Table, Tag, theme, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {deleteWatchlist} from "../../utils/api";
|
||||
import {CalendarFilled, DeleteFilled, DisconnectOutlined, LinkOutlined} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import useBreakpoint from "../../hooks/useBreakpoint";
|
||||
import {actionToColor, domainEvent} from "../search/EventTimeline";
|
||||
import {Watchlist} from "../../pages/tracking/WatchlistPage";
|
||||
import punycode from "punycode/punycode";
|
||||
|
||||
const {useToken} = theme;
|
||||
|
||||
export function WatchlistsList({watchlists, onDelete}: { watchlists: Watchlist[], onDelete: () => void }) {
|
||||
const {token} = useToken()
|
||||
const sm = useBreakpoint('sm')
|
||||
|
||||
const domainEventTranslated = domainEvent()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t`Domain names`,
|
||||
dataIndex: 'domains'
|
||||
},
|
||||
{
|
||||
title: t`Tracked events`,
|
||||
dataIndex: 'events'
|
||||
}
|
||||
]
|
||||
|
||||
return <>
|
||||
{watchlists.map(watchlist =>
|
||||
<>
|
||||
<Card
|
||||
hoverable
|
||||
title={<>
|
||||
{
|
||||
watchlist.connector ?
|
||||
<Tag icon={<LinkOutlined/>} color="lime-inverse" title={watchlist.connector.id}/> :
|
||||
<Tag icon={<DisconnectOutlined/>} color="default"
|
||||
title={t`This Watchlist is not linked to a Connector.`}/>
|
||||
}
|
||||
<Typography.Text title={new Date(watchlist.createdAt).toLocaleString()}>
|
||||
{t`Watchlist` + (watchlist.name ? ` (${watchlist.name})` : '')}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
size='small'
|
||||
style={{width: '100%'}}
|
||||
extra={<Space size='middle'>
|
||||
<Typography.Link href={`/api/watchlists/${watchlist.token}/calendar`}>
|
||||
<CalendarFilled title={t`Export events to iCalendar format`}/>
|
||||
</Typography.Link>
|
||||
<Popconfirm
|
||||
title={t`Delete the Watchlist`}
|
||||
description={t`Are you sure to delete this Watchlist?`}
|
||||
onConfirm={() => deleteWatchlist(watchlist.token).then(onDelete)}
|
||||
okText={t`Yes`}
|
||||
cancelText={t`No`}
|
||||
okButtonProps={{danger: true}}>
|
||||
<DeleteFilled style={{color: token.colorError}} title={t`Delete the Watchlist`}/>
|
||||
</Popconfirm>
|
||||
</Space>}
|
||||
>
|
||||
<Card.Meta description={watchlist.token} style={{marginBottom: '1em'}}/>
|
||||
<Table
|
||||
size='small'
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
style={{width: '100%'}}
|
||||
dataSource={[{
|
||||
domains: watchlist.domains.map(d => <Tag>{punycode.toUnicode(d.ldhName)}</Tag>),
|
||||
events: watchlist.triggers?.filter(t => t.action === 'email')
|
||||
.map(t => <Tag color={actionToColor(t.event)}>
|
||||
{domainEventTranslated[t.event as keyof typeof domainEventTranslated]}
|
||||
</Tag>
|
||||
)
|
||||
}]}
|
||||
{...(sm ? {scroll: {y: 'max-content'}} : {scroll: {y: 240}})}
|
||||
/>
|
||||
</Card>
|
||||
<Divider/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Button, Checkbox, Form, FormInstance, Input, Popconfirm, Select, Space, Typography} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {Connector, ConnectorProvider} from "../../utils/api/connectors";
|
||||
import {Connector, ConnectorProvider} from "../../../utils/api/connectors";
|
||||
import {t} from "ttag";
|
||||
import {BankOutlined} from "@ant-design/icons";
|
||||
import {
|
||||
@@ -8,15 +8,15 @@ import {
|
||||
ovhFields as ovhFieldsFunction,
|
||||
ovhPricingMode as ovhPricingModeFunction,
|
||||
ovhSubsidiaryList as ovhSubsidiaryListFunction
|
||||
} from "../../utils/providers/ovh";
|
||||
import {helpGetTokenLink, tosHyperlink} from "../../utils/providers";
|
||||
} from "../../../utils/providers/ovh";
|
||||
import {helpGetTokenLink, tosHyperlink} from "../../../utils/providers";
|
||||
|
||||
const formItemLayoutWithOutLabel = {
|
||||
wrapperCol: {
|
||||
xs: {span: 24, offset: 0},
|
||||
sm: {span: 20, offset: 4},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate: (values: Connector) => void }) {
|
||||
const [provider, setProvider] = useState<string>()
|
||||
@@ -186,7 +186,7 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
|
||||
</>
|
||||
}
|
||||
|
||||
<Form.Item style={{marginTop: 10}}>
|
||||
<Form.Item style={{marginTop: '5vh'}}>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t`Create`}
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Card, Divider, Popconfirm, theme, Typography} from "antd";
|
||||
import {Card, Divider, message, Popconfirm, theme, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {DeleteFilled} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import {Connector, deleteConnector} from "../../utils/api/connectors";
|
||||
import {Connector, deleteConnector} from "../../../utils/api/connectors";
|
||||
|
||||
const {useToken} = theme;
|
||||
|
||||
@@ -11,17 +11,23 @@ export type ConnectorElement = Connector & { id: string, createdAt: string }
|
||||
|
||||
export function ConnectorsList({connectors, onDelete}: { connectors: ConnectorElement[], onDelete: () => void }) {
|
||||
const {token} = useToken()
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
|
||||
const onConnectorDelete = (connector: ConnectorElement) => deleteConnector(connector.id)
|
||||
.then(onDelete)
|
||||
.catch(() => messageApi.error(t`An error occurred while deleting the Connector. Make sure it is not used in any Watchlist`))
|
||||
|
||||
return <>
|
||||
{connectors.map(connector =>
|
||||
<>
|
||||
{contextHolder}
|
||||
<Card hoverable title={<Typography.Text
|
||||
title={new Date(connector.createdAt).toLocaleString()}>{t`Connector ${connector.provider}`}</Typography.Text>}
|
||||
size='small'
|
||||
style={{width: '100%'}}
|
||||
extra={<Popconfirm title={t`Delete the Connector`}
|
||||
description={t`Are you sure to delete this Connector?`}
|
||||
onConfirm={() => deleteConnector(connector.id).then(onDelete)}
|
||||
onConfirm={() => onConnectorDelete(connector)}
|
||||
okText={t`Yes`}
|
||||
cancelText={t`No`}
|
||||
><DeleteFilled style={{color: token.colorError}}/></Popconfirm>}>
|
||||
@@ -0,0 +1,22 @@
|
||||
import {CalendarFilled} from "@ant-design/icons";
|
||||
import {t} from "ttag";
|
||||
import {Popover, QRCode, Typography} from "antd";
|
||||
import React from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
|
||||
export function CalendarWatchlistButton({watchlist}: { watchlist: Watchlist }) {
|
||||
|
||||
const icsResourceLink = `${window.location.origin}/api/watchlists/${watchlist.token}/calendar`
|
||||
|
||||
return <Typography.Link href={icsResourceLink}>
|
||||
<Popover content={<QRCode value={icsResourceLink}
|
||||
bordered={false}
|
||||
title={t`QR Code for iCalendar export`}
|
||||
type='svg'
|
||||
/>}>
|
||||
<CalendarFilled title={t`Export events to iCalendar format`}
|
||||
style={{color: 'limegreen'}}
|
||||
/>
|
||||
</Popover>
|
||||
</Typography.Link>
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import {Popconfirm, theme, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {deleteWatchlist} from "../../../utils/api";
|
||||
import {DeleteFilled} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
|
||||
export function DeleteWatchlistButton({watchlist, onDelete}: { watchlist: Watchlist, onDelete: () => void }) {
|
||||
const {token} = theme.useToken()
|
||||
|
||||
return <Popconfirm
|
||||
title={t`Delete the Watchlist`}
|
||||
description={t`Are you sure to delete this Watchlist?`}
|
||||
onConfirm={() => deleteWatchlist(watchlist.token).then(onDelete)}
|
||||
okText={t`Yes`}
|
||||
cancelText={t`No`}
|
||||
okButtonProps={{danger: true}}>
|
||||
<Typography.Link>
|
||||
<DeleteFilled style={{color: token.colorError}} title={t`Delete the Watchlist`}/>
|
||||
</Typography.Link>
|
||||
</Popconfirm>
|
||||
}
|
||||
79
assets/components/tracking/watchlist/TrackedDomainTable.tsx
Normal file
79
assets/components/tracking/watchlist/TrackedDomainTable.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Domain, getTrackedDomainList} from "../../../utils/api";
|
||||
import {Table, Tag, Tooltip} from "antd";
|
||||
import {t} from "ttag";
|
||||
import useBreakpoint from "../../../hooks/useBreakpoint";
|
||||
import {ColumnType} from "antd/es/table";
|
||||
import {rdapStatusCodeDetailTranslation} from "../../../utils/functions/rdapTranslation";
|
||||
import {eppStatusCodeToColor} from "../../../utils/functions/eppStatusCodeToColor";
|
||||
|
||||
export function TrackedDomainTable() {
|
||||
const sm = useBreakpoint('sm')
|
||||
const [dataTable, setDataTable] = useState<Domain[]>([])
|
||||
const [total, setTotal] = useState()
|
||||
|
||||
|
||||
const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation()
|
||||
|
||||
const fetchData = (params: { page: number, itemsPerPage: number }) => {
|
||||
getTrackedDomainList(params).then(data => {
|
||||
setTotal(data['hydra:totalItems'])
|
||||
setDataTable(data['hydra:member'].map((d: Domain) => {
|
||||
const expirationDate = d.events.find(e => e.action === 'expiration' && !e.deleted)?.date
|
||||
|
||||
return {
|
||||
key: d.ldhName,
|
||||
ldhName: d.ldhName,
|
||||
expirationDate: expirationDate ? new Date(expirationDate).toLocaleString() : '-',
|
||||
status: d.status.map(s => <Tooltip
|
||||
placement='bottomLeft'
|
||||
title={s in rdapStatusCodeDetailTranslated ? rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] : undefined}>
|
||||
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
|
||||
</Tooltip>
|
||||
),
|
||||
updatedAt: new Date(d.updatedAt).toLocaleString()
|
||||
}
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData({page: 1, itemsPerPage: 30})
|
||||
}, [])
|
||||
|
||||
const columns: ColumnType<any>[] = [
|
||||
{
|
||||
title: t`Domain`,
|
||||
dataIndex: "ldhName"
|
||||
},
|
||||
{
|
||||
title: t`Expiration date`,
|
||||
dataIndex: 'expirationDate'
|
||||
},
|
||||
{
|
||||
title: t`Status`,
|
||||
dataIndex: 'status'
|
||||
},
|
||||
{
|
||||
title: t`Updated at`,
|
||||
dataIndex: 'updatedAt'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
return <Table
|
||||
loading={total === undefined}
|
||||
columns={columns}
|
||||
dataSource={dataTable}
|
||||
pagination={{
|
||||
total,
|
||||
hideOnSinglePage: true,
|
||||
defaultPageSize: 30,
|
||||
onChange: (page, itemsPerPage) => {
|
||||
fetchData({page, itemsPerPage})
|
||||
}
|
||||
}}
|
||||
|
||||
{...(sm ? {scroll: {y: 'max-content'}} : {scroll: {y: 240}})}
|
||||
/>
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import {Button, Drawer, Form, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {WatchlistForm} from "./WatchlistForm";
|
||||
import React, {useState} from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
import {EditOutlined} from "@ant-design/icons";
|
||||
import {Connector} from "../../../utils/api/connectors";
|
||||
|
||||
export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}: {
|
||||
watchlist: Watchlist,
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
|
||||
connectors: (Connector & { id: string })[]
|
||||
}) {
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
|
||||
const showDrawer = () => {
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
setOpen(false)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return <>
|
||||
<Typography.Link>
|
||||
<EditOutlined title={t`Edit the Watchlist`} onClick={() => {
|
||||
showDrawer()
|
||||
form.setFields([
|
||||
{name: 'token', value: watchlist.token},
|
||||
{name: 'name', value: watchlist.name},
|
||||
{name: 'connector', value: watchlist.connector?.id},
|
||||
{name: 'domains', value: watchlist.domains.map(d => d.ldhName)},
|
||||
{name: 'triggers', value: [...new Set(watchlist.triggers?.map(t => t.event))]},
|
||||
{name: 'dsn', value: watchlist.dsn}
|
||||
])
|
||||
}}/>
|
||||
</Typography.Link>
|
||||
<Drawer
|
||||
title={t`Update a Watchlist`}
|
||||
width='80%'
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
loading={loading}
|
||||
styles={{
|
||||
body: {
|
||||
paddingBottom: 80,
|
||||
}
|
||||
}}
|
||||
extra={<Button onClick={onClose}>{t`Cancel`}</Button>}
|
||||
>
|
||||
<WatchlistForm
|
||||
form={form}
|
||||
onFinish={values => {
|
||||
setLoading(true)
|
||||
onUpdateWatchlist(values).then(onClose).catch(() => setLoading(false))
|
||||
}}
|
||||
connectors={connectors}
|
||||
isCreation={false}
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
|
||||
}
|
||||
96
assets/components/tracking/watchlist/WatchlistCard.tsx
Normal file
96
assets/components/tracking/watchlist/WatchlistCard.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import {Card, Divider, Space, Table, Tag, Tooltip} from "antd";
|
||||
import {DisconnectOutlined, LinkOutlined} from "@ant-design/icons";
|
||||
import {t} from "ttag";
|
||||
import {ViewDiagramWatchlistButton} from "./diagram/ViewDiagramWatchlistButton";
|
||||
import {UpdateWatchlistButton} from "./UpdateWatchlistButton";
|
||||
import {DeleteWatchlistButton} from "./DeleteWatchlistButton";
|
||||
import punycode from "punycode/punycode";
|
||||
import React from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
import {Connector} from "../../../utils/api/connectors";
|
||||
import useBreakpoint from "../../../hooks/useBreakpoint";
|
||||
import {CalendarWatchlistButton} from "./CalendarWatchlistButton";
|
||||
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../../utils/functions/rdapTranslation";
|
||||
|
||||
import {actionToColor} from "../../../utils/functions/actionToColor";
|
||||
|
||||
export function WatchlistCard({watchlist, onUpdateWatchlist, connectors, onDelete}: {
|
||||
watchlist: Watchlist,
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
|
||||
connectors: (Connector & { id: string })[],
|
||||
onDelete: () => void
|
||||
}) {
|
||||
const sm = useBreakpoint('sm')
|
||||
const rdapEventNameTranslated = rdapEventNameTranslation()
|
||||
const rdapEventDetailTranslated = rdapEventDetailTranslation()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t`Domain names`,
|
||||
dataIndex: 'domains'
|
||||
},
|
||||
{
|
||||
title: t`Tracked events`,
|
||||
dataIndex: 'events'
|
||||
}
|
||||
]
|
||||
|
||||
return <>
|
||||
<Card
|
||||
type='inner'
|
||||
title={<>
|
||||
{
|
||||
watchlist.connector ?
|
||||
<Tooltip title={watchlist.connector.id}>
|
||||
<Tag icon={<LinkOutlined/>} color="lime-inverse"/>
|
||||
</Tooltip> :
|
||||
<Tooltip title={t`This Watchlist is not linked to a Connector.`}>
|
||||
<Tag icon={<DisconnectOutlined/>} color="default"/>
|
||||
</Tooltip>
|
||||
}
|
||||
<Tooltip title={new Date(watchlist.createdAt).toLocaleString()}>
|
||||
{t`Watchlist` + (watchlist.name ? ` (${watchlist.name})` : '')}
|
||||
</Tooltip>
|
||||
</>
|
||||
}
|
||||
size='small'
|
||||
style={{width: '100%'}}
|
||||
extra={
|
||||
<Space size='middle'>
|
||||
<ViewDiagramWatchlistButton token={watchlist.token}/>
|
||||
|
||||
<CalendarWatchlistButton watchlist={watchlist}/>
|
||||
|
||||
<UpdateWatchlistButton
|
||||
watchlist={watchlist}
|
||||
onUpdateWatchlist={onUpdateWatchlist}
|
||||
connectors={connectors}
|
||||
/>
|
||||
|
||||
<DeleteWatchlistButton watchlist={watchlist} onDelete={onDelete}/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Card.Meta description={watchlist.token} style={{marginBottom: '1em'}}/>
|
||||
<Table
|
||||
size='small'
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
style={{width: '100%'}}
|
||||
dataSource={[{
|
||||
domains: watchlist.domains.map(d => <Tag>{punycode.toUnicode(d.ldhName)}</Tag>),
|
||||
events: watchlist.triggers?.filter(t => t.action === 'email')
|
||||
.map(t => <Tooltip
|
||||
title={t.event in rdapEventDetailTranslated ? rdapEventDetailTranslated[t.event as keyof typeof rdapEventDetailTranslated] : undefined}>
|
||||
<Tag color={actionToColor(t.event)}>
|
||||
{rdapEventNameTranslated[t.event as keyof typeof rdapEventNameTranslated]}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
}]}
|
||||
{...(sm ? {scroll: {y: 'max-content'}} : {scroll: {y: 240}})}
|
||||
/>
|
||||
</Card>
|
||||
<Divider/>
|
||||
</>
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag} from "antd";
|
||||
import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag, Tooltip, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {ApiOutlined, MinusCircleOutlined, PlusOutlined} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import {Connector} from "../../utils/api/connectors";
|
||||
import {actionToColor, domainEvent} from "../search/EventTimeline";
|
||||
import {Connector} from "../../../utils/api/connectors";
|
||||
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../../utils/functions/rdapTranslation";
|
||||
import {actionToColor} from "../../../utils/functions/actionToColor";
|
||||
import {actionToIcon} from "../../../utils/functions/actionToIcon";
|
||||
|
||||
type TagRender = SelectProps['tagRender'];
|
||||
|
||||
@@ -25,38 +27,48 @@ const formItemLayoutWithOutLabel = {
|
||||
},
|
||||
};
|
||||
|
||||
export function WatchlistForm({form, connectors, onCreateWatchlist}: {
|
||||
export function WatchlistForm({form, connectors, onFinish, isCreation}: {
|
||||
form: FormInstance,
|
||||
connectors: (Connector & { id: string })[]
|
||||
onCreateWatchlist: (values: { domains: string[], emailTriggers: string[] }) => void
|
||||
onFinish: (values: { domains: string[], triggers: string[], token: string }) => void
|
||||
isCreation: boolean
|
||||
}) {
|
||||
const domainEventTranslated = domainEvent()
|
||||
const rdapEventNameTranslated = rdapEventNameTranslation()
|
||||
const rdapEventDetailTranslated = rdapEventDetailTranslation()
|
||||
|
||||
const triggerTagRenderer: TagRender = (props) => {
|
||||
const {value, closable, onClose} = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
return (
|
||||
<Tag
|
||||
color={actionToColor(value)}
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{marginInlineEnd: 4}}
|
||||
>
|
||||
{domainEventTranslated[value as keyof typeof domainEventTranslated]}
|
||||
</Tag>
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
return (<Tooltip
|
||||
title={value in rdapEventDetailTranslated ? rdapEventDetailTranslated[value as keyof typeof rdapEventDetailTranslated] : undefined}>
|
||||
<Tag
|
||||
icon={actionToIcon(value)}
|
||||
color={actionToColor(value)}
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{marginInlineEnd: 4}}
|
||||
>
|
||||
{rdapEventNameTranslated[value as keyof typeof rdapEventNameTranslated]}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return <Form
|
||||
{...formItemLayoutWithOutLabel}
|
||||
form={form}
|
||||
onFinish={onCreateWatchlist}
|
||||
initialValues={{emailTriggers: ['last changed', 'transfer', 'expiration', 'deletion']}}
|
||||
onFinish={onFinish}
|
||||
initialValues={{triggers: ['last changed', 'transfer', 'expiration', 'deletion']}}
|
||||
>
|
||||
|
||||
<Form.Item name='token' hidden>
|
||||
<Input hidden/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t`Name`}
|
||||
name='name'
|
||||
labelCol={{
|
||||
@@ -134,7 +146,7 @@ export function WatchlistForm({form, connectors, onCreateWatchlist}: {
|
||||
)}
|
||||
</Form.List>
|
||||
<Form.Item label={t`Tracked events`}
|
||||
name='emailTriggers'
|
||||
name='triggers'
|
||||
rules={[{required: true, message: t`At least one trigger`, type: 'array'}]}
|
||||
labelCol={{
|
||||
xs: {span: 24},
|
||||
@@ -150,9 +162,10 @@ export function WatchlistForm({form, connectors, onCreateWatchlist}: {
|
||||
mode="multiple"
|
||||
tagRender={triggerTagRenderer}
|
||||
style={{width: '100%'}}
|
||||
options={Object.keys(domainEventTranslated).map(e => ({
|
||||
options={Object.keys(rdapEventNameTranslated).map(e => ({
|
||||
value: e,
|
||||
label: domainEventTranslated[e as keyof typeof domainEventTranslated]
|
||||
title: e in rdapEventDetailTranslated ? rdapEventDetailTranslated[e as keyof typeof rdapEventDetailTranslated] : undefined,
|
||||
label: rdapEventNameTranslated[e as keyof typeof rdapEventNameTranslated]
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -180,10 +193,63 @@ export function WatchlistForm({form, connectors, onCreateWatchlist}: {
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Form.List
|
||||
name="dsn">
|
||||
{(fields, {add, remove}, {errors}) => (
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item
|
||||
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
|
||||
label={index === 0 ? t`DSN` : ''}
|
||||
required={true}
|
||||
key={field.key}
|
||||
>
|
||||
<Form.Item
|
||||
{...field}
|
||||
validateTrigger={['onChange', 'onBlur']}
|
||||
rules={[{
|
||||
required: true,
|
||||
message: t`Required`
|
||||
}, {
|
||||
pattern: /:\/\//,
|
||||
message: t`This DSN does not appear to be valid`
|
||||
}]}
|
||||
noStyle
|
||||
>
|
||||
<Input placeholder={'slack://TOKEN@default?channel=CHANNEL'} style={{width: '60%'}}
|
||||
autoComplete='off'/>
|
||||
</Form.Item>
|
||||
{fields.length > 0 ? (
|
||||
<MinusCircleOutlined
|
||||
className="dynamic-delete-button"
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
) : null}
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item help={
|
||||
<Typography.Link href='https://symfony.com/doc/current/notifier.html#chat-channel'
|
||||
target='_blank'>
|
||||
{t`Check out this link to the Symfony documentation to help you build the DSN`}
|
||||
</Typography.Link>}
|
||||
>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
style={{width: '60%'}}
|
||||
icon={<PlusOutlined/>}
|
||||
>
|
||||
{t`Add a Webhook`}
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Form.Item style={{marginTop: '5vh'}}>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t`Create`}
|
||||
{isCreation ? t`Create` : t`Update`}
|
||||
</Button>
|
||||
<Button type="default" htmlType="reset">
|
||||
{t`Reset`}
|
||||
23
assets/components/tracking/watchlist/WatchlistsList.tsx
Normal file
23
assets/components/tracking/watchlist/WatchlistsList.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
import {Connector} from "../../../utils/api/connectors";
|
||||
import {WatchlistCard} from "./WatchlistCard";
|
||||
|
||||
export function WatchlistsList({watchlists, onDelete, onUpdateWatchlist, connectors}: {
|
||||
watchlists: Watchlist[],
|
||||
onDelete: () => void,
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
|
||||
connectors: (Connector & { id: string })[]
|
||||
}) {
|
||||
|
||||
|
||||
return <>
|
||||
{watchlists.map(watchlist =>
|
||||
<WatchlistCard watchlist={watchlist}
|
||||
onUpdateWatchlist={onUpdateWatchlist}
|
||||
connectors={connectors}
|
||||
onDelete={onDelete}/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import {Button, Flex, Modal, Space, Typography} from "antd"
|
||||
import {t} from "ttag"
|
||||
import React, {useEffect, useState} from "react"
|
||||
import {ApartmentOutlined} from "@ant-design/icons"
|
||||
|
||||
import '@xyflow/react/dist/style.css'
|
||||
import {Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState} from "@xyflow/react";
|
||||
import {getWatchlist} from "../../../../utils/api";
|
||||
import {getLayoutedElements} from "./getLayoutedElements";
|
||||
import {watchlistToNodes} from "./watchlistToNodes";
|
||||
import {watchlistToEdges} from "./watchlistToEdges";
|
||||
|
||||
export function ViewDiagramWatchlistButton({token}: { token: string }) {
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([])
|
||||
|
||||
useEffect(() => {
|
||||
setEdges([])
|
||||
setNodes([])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
setLoading(true)
|
||||
getWatchlist(token).then(w => {
|
||||
const e = getLayoutedElements(watchlistToNodes(w, true), watchlistToEdges(w, true))
|
||||
setNodes(e.nodes)
|
||||
setEdges(e.edges)
|
||||
}).catch(() => setOpen(false)).finally(() => setLoading(false))
|
||||
|
||||
}, [open])
|
||||
|
||||
|
||||
return <>
|
||||
<Typography.Link>
|
||||
<ApartmentOutlined title={t`View the Watchlist Entity Diagram`}
|
||||
style={{color: 'darkviolet'}}
|
||||
onClick={() => setOpen(true)}/>
|
||||
</Typography.Link>
|
||||
<Modal
|
||||
title={t`Watchlist Entity Diagram`}
|
||||
centered
|
||||
open={open}
|
||||
loading={loading}
|
||||
footer={
|
||||
<Space>
|
||||
<Button type="default" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
onOk={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
width='90vw'
|
||||
height='100%'
|
||||
>
|
||||
<Flex style={{width: '85vw', height: '85vh'}}>
|
||||
<ReactFlow
|
||||
fitView
|
||||
colorMode='dark'
|
||||
defaultEdges={[]}
|
||||
defaultNodes={[]}
|
||||
nodesConnectable={false}
|
||||
edgesReconnectable={false}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
style={{width: '100%', height: '100%'}}
|
||||
>
|
||||
<MiniMap/>
|
||||
<Controls/>
|
||||
<Background/>
|
||||
</ReactFlow>
|
||||
</Flex>
|
||||
</Modal>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import dagre from "dagre"
|
||||
|
||||
export const getLayoutedElements = (nodes: any, edges: any, direction = 'TB') => {
|
||||
const dagreGraph = new dagre.graphlib.Graph()
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
||||
|
||||
const nodeWidth = 172
|
||||
const nodeHeight = 200
|
||||
|
||||
const isHorizontal = direction === 'LR';
|
||||
dagreGraph.setGraph({rankdir: direction});
|
||||
|
||||
nodes.forEach((node: any) => {
|
||||
dagreGraph.setNode(node.id, {width: nodeWidth, height: nodeHeight});
|
||||
});
|
||||
|
||||
edges.forEach((edge: any) => {
|
||||
dagreGraph.setEdge(edge.source, edge.target);
|
||||
});
|
||||
|
||||
dagre.layout(dagreGraph);
|
||||
|
||||
const newNodes = nodes.map((node: any) => {
|
||||
const nodeWithPosition = dagreGraph.node(node.id)
|
||||
|
||||
return {
|
||||
...node,
|
||||
targetPosition: isHorizontal ? 'left' : 'top',
|
||||
sourcePosition: isHorizontal ? 'right' : 'bottom',
|
||||
position: {
|
||||
x: nodeWithPosition.x - nodeWidth / 2,
|
||||
y: nodeWithPosition.y - nodeHeight / 2
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {nodes: newNodes, edges};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import {Domain, Watchlist} from "../../../../utils/api";
|
||||
import {rdapRoleTranslation} from "../../../../utils/functions/rdapTranslation";
|
||||
import {t} from "ttag";
|
||||
|
||||
import {rolesToColor} from "../../../../utils/functions/rolesToColor";
|
||||
|
||||
export function domainEntitiesToEdges(d: Domain, withRegistrar = false) {
|
||||
const rdapRoleTranslated = rdapRoleTranslation()
|
||||
return d.entities
|
||||
.filter(e => !e.deleted && (!withRegistrar ? !e.roles.includes('registrar') : true))
|
||||
.map(e => ({
|
||||
id: `e-${d.ldhName}-${e.entity.handle}`,
|
||||
source: e.roles.includes('registrant') || e.roles.includes('registrar') ? e.entity.handle : d.ldhName,
|
||||
target: e.roles.includes('registrant') || e.roles.includes('registrar') ? d.ldhName : e.entity.handle,
|
||||
style: {stroke: rolesToColor(e.roles), strokeWidth: 3},
|
||||
label: e.roles
|
||||
.map(r => r in rdapRoleTranslated ? rdapRoleTranslated[r as keyof typeof rdapRoleTranslated] : r)
|
||||
.join(', '),
|
||||
animated: e.roles.includes('registrant'),
|
||||
}))
|
||||
}
|
||||
|
||||
export const domainNSToEdges = (d: Domain) => d.nameservers
|
||||
.map(ns => ({
|
||||
id: `ns-${d.ldhName}-${ns.ldhName}`,
|
||||
source: d.ldhName,
|
||||
target: ns.ldhName,
|
||||
style: {stroke: 'grey', strokeWidth: 3},
|
||||
label: 'DNS'
|
||||
}))
|
||||
|
||||
export const tldToEdge = (d: Domain) => ({
|
||||
id: `tld-${d.ldhName}-${d.tld.tld}`,
|
||||
source: d.tld.tld,
|
||||
target: d.ldhName,
|
||||
style: {stroke: 'yellow', strokeWidth: 3},
|
||||
label: t`Registry`
|
||||
})
|
||||
|
||||
export function watchlistToEdges(watchlist: Watchlist, withRegistrar = false, withTld = false) {
|
||||
const entitiesEdges = watchlist.domains.map(d => domainEntitiesToEdges(d, withRegistrar)).flat()
|
||||
const nameserversEdges = watchlist.domains.map(domainNSToEdges).flat()
|
||||
const tldEdge = watchlist.domains.map(tldToEdge)
|
||||
|
||||
return [...entitiesEdges, ...nameserversEdges, ...(withTld ? tldEdge : [])]
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import {Domain, Nameserver, Tld, Watchlist} from "../../../../utils/api";
|
||||
import React from "react";
|
||||
import {t} from 'ttag'
|
||||
|
||||
import {entityToName} from "../../../../utils/functions/entityToName";
|
||||
|
||||
export const domainToNode = (d: Domain) => ({
|
||||
id: d.ldhName,
|
||||
data: {label: <b>{d.ldhName}</b>},
|
||||
style: {
|
||||
width: 200
|
||||
}
|
||||
})
|
||||
|
||||
export const domainEntitiesToNode = (d: Domain, withRegistrar = false) => d.entities
|
||||
.filter(e => !e.deleted && (!withRegistrar ? !e.roles.includes('registrar') : true))
|
||||
.map(e => {
|
||||
return {
|
||||
id: e.entity.handle,
|
||||
type: e.roles.includes('registrant') || e.roles.includes('registrar') ? 'input' : 'output',
|
||||
data: {label: entityToName(e)},
|
||||
style: {
|
||||
width: 200
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const tldToNode = (tld: Tld) => ({
|
||||
id: tld.tld,
|
||||
data: {label: t`.${tld.tld} Registry`},
|
||||
type: 'input',
|
||||
style: {
|
||||
width: 200
|
||||
}
|
||||
})
|
||||
|
||||
export const nsToNode = (ns: Nameserver) => ({
|
||||
id: ns.ldhName,
|
||||
data: {label: ns.ldhName},
|
||||
type: 'output',
|
||||
style: {
|
||||
width: 200
|
||||
}
|
||||
})
|
||||
|
||||
export function watchlistToNodes(watchlist: Watchlist, withRegistrar = false, withTld = false) {
|
||||
|
||||
const domains = watchlist.domains.map(domainToNode)
|
||||
const entities = [...new Set(watchlist.domains.map(d => domainEntitiesToNode(d, withRegistrar)).flat())]
|
||||
const tlds = [...new Set(watchlist.domains.map(d => d.tld))].map(tldToNode)
|
||||
const nameservers = [...new Set(watchlist.domains.map(d => d.nameservers))].flat().map(nsToNode, withRegistrar)
|
||||
|
||||
return [...domains, ...entities, ...nameservers, ...(withTld ? tlds : [])]
|
||||
}
|
||||
@@ -5,10 +5,10 @@ export const regionNames = new Intl.DisplayNames([locale], {type: 'region'})
|
||||
|
||||
if (locale !== 'en') {
|
||||
fetch(`/locales/${locale}.po.json`).then(response => {
|
||||
if (!response.ok) throw new Error(`Failed to load translations for locale ${locale}`);
|
||||
if (!response.ok) throw new Error(`Failed to load translations for locale ${locale}`)
|
||||
response.json().then(translationsObj => {
|
||||
addLocale(locale, translationsObj);
|
||||
useLocale(locale);
|
||||
addLocale(locale, translationsObj)
|
||||
useLocale(locale)
|
||||
})
|
||||
})
|
||||
}).catch(() => console.error(`Unable to retrieve translation file ${locale}.po.json`))
|
||||
}
|
||||
|
||||
@@ -7,11 +7,6 @@ import {getConfiguration, InstanceConfig} from "../utils/api";
|
||||
import {RegisterForm} from "../components/RegisterForm";
|
||||
|
||||
|
||||
const gridStyle: React.CSSProperties = {
|
||||
width: '50%',
|
||||
textAlign: 'center',
|
||||
}
|
||||
|
||||
export const AuthenticatedContext = createContext<any>(null)
|
||||
|
||||
export default function LoginPage() {
|
||||
@@ -28,7 +23,7 @@ export default function LoginPage() {
|
||||
}, [])
|
||||
|
||||
return <Card title={wantRegister ? t`Register` : t`Log in`} style={{width: '100%'}}>
|
||||
<Card.Grid style={gridStyle} hoverable={false}>
|
||||
<Card.Grid style={{width: '50%', textAlign: 'center'}} hoverable={false}>
|
||||
{wantRegister ? <RegisterForm/> : <LoginForm ssoLogin={configuration?.ssoLogin}/>}
|
||||
{
|
||||
configuration?.registerEnabled &&
|
||||
|
||||
120
assets/pages/StatisticsPage.tsx
Normal file
120
assets/pages/StatisticsPage.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {getStatistics, Statistics} from "../utils/api";
|
||||
import {Card, Col, Divider, Row, Statistic, Tooltip} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {
|
||||
AimOutlined,
|
||||
CompassOutlined,
|
||||
DatabaseOutlined,
|
||||
FieldTimeOutlined,
|
||||
NotificationOutlined
|
||||
} from "@ant-design/icons";
|
||||
|
||||
export default function StatisticsPage() {
|
||||
|
||||
const [stats, setStats] = useState<Statistics>()
|
||||
|
||||
useEffect(() => {
|
||||
getStatistics().then(setStats)
|
||||
}, [])
|
||||
|
||||
const totalDomainPurchase = (stats?.domainPurchased ?? 0) + (stats?.domainPurchaseFailed ?? 0)
|
||||
|
||||
const successRate = stats !== undefined ?
|
||||
(totalDomainPurchase === 0 ? undefined : stats.domainPurchased / totalDomainPurchase)
|
||||
: undefined
|
||||
|
||||
return <>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card bordered={false}>
|
||||
<Statistic
|
||||
loading={stats === undefined}
|
||||
prefix={<CompassOutlined/>}
|
||||
title={t`RDAP queries`}
|
||||
value={stats?.rdapQueries}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card bordered={false}>
|
||||
<Statistic
|
||||
loading={stats === undefined}
|
||||
title={t`Alerts sent`}
|
||||
prefix={<NotificationOutlined/>}
|
||||
value={stats?.alertSent}
|
||||
valueStyle={{color: 'blueviolet'}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider/>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card bordered={false}>
|
||||
<Statistic
|
||||
loading={stats === undefined}
|
||||
title={t`Domain names in database`}
|
||||
prefix={<DatabaseOutlined/>}
|
||||
value={stats?.domainCountTotal}
|
||||
valueStyle={{color: 'darkblue'}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card bordered={false}>
|
||||
<Statistic
|
||||
loading={stats === undefined}
|
||||
title={t`Tracked domain names`}
|
||||
prefix={<AimOutlined/>}
|
||||
value={stats?.domainTracked}
|
||||
valueStyle={{color: 'darkviolet'}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider/>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card bordered={false}>
|
||||
<Statistic
|
||||
loading={stats === undefined}
|
||||
title={t`Purchased domain names`}
|
||||
prefix={<FieldTimeOutlined/>}
|
||||
value={stats?.domainPurchased}
|
||||
valueStyle={{color: '#3f8600'}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card bordered={false}>
|
||||
<Tooltip
|
||||
title={t`This value is based on the status code of the HTTP response from the providers following the domain order.`}>
|
||||
<Statistic
|
||||
loading={stats === undefined}
|
||||
title={t`Success rate`}
|
||||
value={successRate === undefined ? '-' : successRate * 100}
|
||||
suffix='%'
|
||||
valueStyle={{color: successRate === undefined ? 'grey' : successRate >= 0.5 ? 'darkgreen' : 'orange'}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider/>
|
||||
<Row gutter={16} justify='center' align='middle'>
|
||||
{stats?.domainCount
|
||||
.sort((a, b) => b.domain - a.domain)
|
||||
.map(({domain, tld}) => <Col span={4}>
|
||||
<Card bordered={false}>
|
||||
<Statistic
|
||||
loading={stats === undefined}
|
||||
title={`.${tld}`}
|
||||
value={domain}
|
||||
valueStyle={{color: 'darkblue'}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>)}
|
||||
</Row>
|
||||
</>
|
||||
}
|
||||
@@ -1,16 +1,29 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import snarkdown from "snarkdown"
|
||||
import {Skeleton} from "antd";
|
||||
import {Skeleton, Typography} from "antd";
|
||||
import axios from "axios";
|
||||
import {t} from "ttag";
|
||||
|
||||
export default function TextPage({resource}: { resource: string }) {
|
||||
const [markdown, setMarkdown] = useState<string>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [markdown, setMarkdown] = useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
axios.get('/content/' + resource).then(res => setMarkdown(res.data))
|
||||
setLoading(true)
|
||||
axios.get('/content/' + resource)
|
||||
.then(res => setMarkdown(res.data))
|
||||
.catch(err => {
|
||||
console.error(`Please create the /public/content/${resource} file.`)
|
||||
setMarkdown(undefined)
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}, [resource])
|
||||
|
||||
return <Skeleton loading={markdown === undefined} active>
|
||||
{markdown !== undefined && <div dangerouslySetInnerHTML={{__html: snarkdown(markdown)}}></div>}
|
||||
return <Skeleton loading={loading} active>
|
||||
{markdown !== undefined ? <div
|
||||
dangerouslySetInnerHTML={{__html: snarkdown(markdown)}}></div> :
|
||||
<Typography.Text strong>
|
||||
{t`📝 Please create the /public/content/${resource} file.`}
|
||||
</Typography.Text>}
|
||||
</Skeleton>
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Card, Flex, Skeleton, Typography} from "antd";
|
||||
import {getUser, User} from "../../utils/api";
|
||||
import {getUser, User} from "../utils/api";
|
||||
import {t} from 'ttag'
|
||||
|
||||
export default function UserPage() {
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default function StatisticsPage() {
|
||||
return <p>
|
||||
Not implemented
|
||||
</p>
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
import React, {useState} from "react";
|
||||
import {Badge, Card, Divider, Empty, Flex, FormProps, message, Skeleton, Space, Tag, Typography} from "antd";
|
||||
import {Empty, Flex, FormProps, message, Skeleton} from "antd";
|
||||
import {Domain, getDomain} from "../../utils/api";
|
||||
import {AxiosError} from "axios"
|
||||
import {t} from 'ttag'
|
||||
import {DomainSearchBar, FieldType} from "../../components/search/DomainSearchBar";
|
||||
import {EventTimeline} from "../../components/search/EventTimeline";
|
||||
import {EntitiesList} from "../../components/search/EntitiesList";
|
||||
import {showErrorAPI} from "../../utils";
|
||||
|
||||
const {Text} = Typography;
|
||||
import {DomainResult} from "../../components/search/DomainResult";
|
||||
import {showErrorAPI} from "../../utils/functions/showErrorAPI";
|
||||
|
||||
export default function DomainSearchPage() {
|
||||
const [domain, setDomain] = useState<Domain | null>()
|
||||
@@ -26,53 +23,16 @@ export default function DomainSearchPage() {
|
||||
}
|
||||
|
||||
return <Flex gap="middle" align="center" justify="center" vertical>
|
||||
<Card title={t`Domain finder`} style={{width: '100%'}}>
|
||||
{contextHolder}
|
||||
<DomainSearchBar onFinish={onFinish}/>
|
||||
{contextHolder}
|
||||
<DomainSearchBar onFinish={onFinish}/>
|
||||
|
||||
<Skeleton loading={domain === null} active>
|
||||
{
|
||||
domain &&
|
||||
(!domain.deleted ? <Space direction="vertical" size="middle" style={{width: '100%'}}>
|
||||
<Badge.Ribbon text={`.${domain.tld.tld.toUpperCase()} (${domain.tld.type})`}
|
||||
color={
|
||||
domain.tld.type === 'ccTLD' ? 'purple' :
|
||||
(domain.tld.type === 'gTLD' && domain.tld.specification13) ? "volcano" :
|
||||
domain.tld.type === 'gTLD' ? "green"
|
||||
: "cyan"
|
||||
}>
|
||||
<Card title={<>
|
||||
{domain.ldhName}{domain.handle && <Text code>{domain.handle}</Text>}
|
||||
</>}
|
||||
size="small">
|
||||
{domain.status.length > 0 &&
|
||||
<>
|
||||
<Divider orientation="left">{t`EPP Status Codes`}</Divider>
|
||||
<Flex gap="4px 0" wrap>
|
||||
{
|
||||
domain.status.map(s =>
|
||||
<Tag color={s === 'active' ? 'green' : 'blue'}>{s}</Tag>
|
||||
)
|
||||
}
|
||||
</Flex>
|
||||
</>
|
||||
}
|
||||
<Divider orientation="left">{t`Timeline`}</Divider>
|
||||
<EventTimeline domain={domain}/>
|
||||
{
|
||||
domain.entities.length > 0 &&
|
||||
<>
|
||||
<Divider orientation="left">{t`Entities`}</Divider>
|
||||
<EntitiesList domain={domain}/>
|
||||
</>
|
||||
}
|
||||
</Card>
|
||||
</Badge.Ribbon>
|
||||
</Space>
|
||||
: <Empty
|
||||
description={t`Although the domain exists in my database, it has been deleted from the WHOIS by its registrar.`}/>)
|
||||
}
|
||||
</Skeleton>
|
||||
</Card>
|
||||
<Skeleton loading={domain === null} active>
|
||||
{
|
||||
domain &&
|
||||
(!domain.deleted ? <DomainResult domain={domain}/>
|
||||
: <Empty
|
||||
description={t`Although the domain exists in my database, it has been deleted from the WHOIS by its registrar.`}/>)
|
||||
}
|
||||
</Skeleton>
|
||||
</Flex>
|
||||
}
|
||||
@@ -6,28 +6,14 @@ import {regionNames} from "../../i18n";
|
||||
import useBreakpoint from "../../hooks/useBreakpoint";
|
||||
import {ColumnType} from "antd/es/table";
|
||||
import punycode from "punycode/punycode";
|
||||
import {getCountryCode} from "../../utils/functions/getCountryCode";
|
||||
import {tldToEmoji} from "../../utils/functions/tldToEmoji";
|
||||
|
||||
const {Text, Paragraph} = Typography
|
||||
|
||||
type TldType = 'iTLD' | 'sTLD' | 'gTLD' | 'ccTLD'
|
||||
type FiltersType = { type: TldType, contractTerminated?: boolean, specification13?: boolean }
|
||||
|
||||
const toEmoji = (tld: string) => {
|
||||
if (tld.startsWith('xn--')) return '-'
|
||||
|
||||
return String.fromCodePoint(
|
||||
...getCountryCode(tld)
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map((char) => 127397 + char.charCodeAt(0))
|
||||
)
|
||||
}
|
||||
|
||||
const getCountryCode = (tld: string): string => {
|
||||
const exceptions = {uk: 'gb', su: 'ru', tp: 'tl'}
|
||||
if (tld in exceptions) return exceptions[tld as keyof typeof exceptions]
|
||||
return tld.toUpperCase()
|
||||
}
|
||||
|
||||
function TldTable(filters: FiltersType) {
|
||||
const sm = useBreakpoint('sm')
|
||||
@@ -55,7 +41,7 @@ function TldTable(filters: FiltersType) {
|
||||
|
||||
return {
|
||||
...rowData,
|
||||
Flag: toEmoji(tld.tld),
|
||||
Flag: tldToEmoji(tld.tld),
|
||||
Country: countryName
|
||||
}
|
||||
case 'gTLD':
|
||||
@@ -127,6 +113,7 @@ export default function TldPage() {
|
||||
</Paragraph>
|
||||
<Divider/>
|
||||
<Collapse
|
||||
accordion
|
||||
size={sm ? 'small' : 'large'}
|
||||
items={[
|
||||
{
|
||||
@@ -2,10 +2,11 @@ import React, {useEffect, useState} from "react";
|
||||
import {Card, Flex, Form, message, Skeleton} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {Connector, getConnectors, postConnector} from "../../utils/api/connectors";
|
||||
import {ConnectorForm} from "../../components/tracking/ConnectorForm";
|
||||
import {ConnectorForm} from "../../components/tracking/connector/ConnectorForm";
|
||||
import {AxiosError} from "axios";
|
||||
import {ConnectorElement, ConnectorsList} from "../../components/tracking/ConnectorsList";
|
||||
import {showErrorAPI} from "../../utils";
|
||||
import {ConnectorElement, ConnectorsList} from "../../components/tracking/connector/ConnectorsList";
|
||||
|
||||
import {showErrorAPI} from "../../utils/functions/showErrorAPI";
|
||||
|
||||
export default function ConnectorsPage() {
|
||||
const [form] = Form.useForm()
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Card, Divider, Flex, Form, message} from "antd";
|
||||
import {EventAction, getWatchlists, postWatchlist} from "../../utils/api";
|
||||
import {Card, Divider, Flex, Form, message, Tag} from "antd";
|
||||
import {EventAction, getWatchlists, postWatchlist, putWatchlist} from "../../utils/api";
|
||||
import {AxiosError} from "axios";
|
||||
import {t} from 'ttag'
|
||||
import {WatchlistForm} from "../../components/tracking/WatchlistForm";
|
||||
import {WatchlistsList} from "../../components/tracking/WatchlistsList";
|
||||
import {WatchlistForm} from "../../components/tracking/watchlist/WatchlistForm";
|
||||
import {WatchlistsList} from "../../components/tracking/watchlist/WatchlistsList";
|
||||
import {Connector, getConnectors} from "../../utils/api/connectors";
|
||||
import {showErrorAPI} from "../../utils";
|
||||
|
||||
import {showErrorAPI} from "../../utils/functions/showErrorAPI";
|
||||
import {TrackedDomainTable} from "../../components/tracking/watchlist/TrackedDomainTable";
|
||||
import {AimOutlined} from "@ant-design/icons";
|
||||
|
||||
|
||||
export type Watchlist = {
|
||||
@@ -14,6 +17,7 @@ export type Watchlist = {
|
||||
token: string,
|
||||
domains: { ldhName: string }[],
|
||||
triggers?: { event: EventAction, action: string }[],
|
||||
dsn?: string[]
|
||||
connector?: {
|
||||
id: string
|
||||
provider: string
|
||||
@@ -22,26 +26,42 @@ export type Watchlist = {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
type FormValuesType = {
|
||||
name?: string
|
||||
domains: string[],
|
||||
triggers: string[]
|
||||
connector?: string,
|
||||
dsn?: string[]
|
||||
}
|
||||
|
||||
const getRequestDataFromForm = (values: FormValuesType) => {
|
||||
const domainsURI = values.domains.map(d => '/api/domains/' + d.toLowerCase())
|
||||
let triggers = values.triggers.map(t => ({event: t, action: 'email'}))
|
||||
|
||||
if (values.dsn !== undefined) {
|
||||
triggers = [...triggers, ...values.triggers.map(t => ({
|
||||
event: t,
|
||||
action: 'chat'
|
||||
}))]
|
||||
}
|
||||
return {
|
||||
name: values.name,
|
||||
domains: domainsURI,
|
||||
triggers,
|
||||
connector: values.connector !== undefined ? ('/api/connectors/' + values.connector) : undefined,
|
||||
dsn: values.dsn
|
||||
}
|
||||
}
|
||||
|
||||
export default function WatchlistPage() {
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const [watchlists, setWatchlists] = useState<Watchlist[] | null>()
|
||||
const [connectors, setConnectors] = useState<(Connector & { id: string })[] | null>()
|
||||
const [watchlists, setWatchlists] = useState<Watchlist[]>()
|
||||
const [connectors, setConnectors] = useState<(Connector & { id: string })[]>()
|
||||
|
||||
const onCreateWatchlist = (values: {
|
||||
name?: string
|
||||
domains: string[],
|
||||
emailTriggers: string[]
|
||||
connector?: string
|
||||
}) => {
|
||||
const domainsURI = values.domains.map(d => '/api/domains/' + d)
|
||||
postWatchlist({
|
||||
name: values.name,
|
||||
domains: domainsURI,
|
||||
triggers: values.emailTriggers.map(t => ({event: t, action: 'email'})),
|
||||
connector: values.connector !== undefined ? '/api/connectors/' + values.connector : undefined
|
||||
}).then((w) => {
|
||||
const onCreateWatchlist = (values: FormValuesType) => {
|
||||
postWatchlist(getRequestDataFromForm(values)).then((w) => {
|
||||
form.resetFields()
|
||||
refreshWatchlists()
|
||||
messageApi.success(t`Watchlist created !`)
|
||||
@@ -50,6 +70,17 @@ export default function WatchlistPage() {
|
||||
})
|
||||
}
|
||||
|
||||
const onUpdateWatchlist = async (values: FormValuesType & { token: string }) => putWatchlist({
|
||||
token: values.token,
|
||||
...getRequestDataFromForm(values)
|
||||
}
|
||||
).then((w) => {
|
||||
refreshWatchlists()
|
||||
messageApi.success(t`Watchlist updated !`)
|
||||
}).catch((e: AxiosError) => {
|
||||
throw showErrorAPI(e, messageApi)
|
||||
})
|
||||
|
||||
const refreshWatchlists = () => getWatchlists().then(w => {
|
||||
setWatchlists(w['hydra:member'])
|
||||
}).catch((e: AxiosError) => {
|
||||
@@ -67,17 +98,27 @@ export default function WatchlistPage() {
|
||||
}, [])
|
||||
|
||||
return <Flex gap="middle" align="center" justify="center" vertical>
|
||||
<Card title={t`Create a Watchlist`} style={{width: '100%'}}>
|
||||
{contextHolder}
|
||||
{
|
||||
connectors &&
|
||||
<WatchlistForm form={form} onCreateWatchlist={onCreateWatchlist} connectors={connectors}/>
|
||||
{contextHolder}
|
||||
<Card loading={connectors === undefined} title={t`Create a Watchlist`} style={{width: '100%'}}>
|
||||
{connectors &&
|
||||
<WatchlistForm form={form} onFinish={onCreateWatchlist} connectors={connectors} isCreation={true}/>
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Divider/>
|
||||
|
||||
{watchlists && watchlists.length > 0 &&
|
||||
<WatchlistsList watchlists={watchlists} onDelete={refreshWatchlists}/>}
|
||||
<Card title={
|
||||
<>
|
||||
<Tag icon={<AimOutlined/>} color="cyan-inverse"/>
|
||||
{t`Tracked domain names`}
|
||||
</>
|
||||
}
|
||||
style={{width: '100%'}}>
|
||||
<TrackedDomainTable/>
|
||||
</Card>
|
||||
<Divider/>
|
||||
{connectors && watchlists && watchlists.length > 0 &&
|
||||
<WatchlistsList watchlists={watchlists} onDelete={refreshWatchlists}
|
||||
connectors={connectors}
|
||||
onUpdateWatchlist={onUpdateWatchlist}
|
||||
/>}
|
||||
</Flex>
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export type TriggerAction = 'email' | string
|
||||
export interface Event {
|
||||
action: EventAction
|
||||
date: string
|
||||
deleted: boolean
|
||||
}
|
||||
|
||||
export interface Entity {
|
||||
@@ -53,10 +54,12 @@ export interface Domain {
|
||||
entity: Entity
|
||||
events: Event[]
|
||||
roles: string[]
|
||||
deleted: boolean
|
||||
}[]
|
||||
nameservers: Nameserver[]
|
||||
tld: Tld
|
||||
deleted: boolean
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface User {
|
||||
@@ -64,11 +67,22 @@ export interface User {
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
export interface Watchlist {
|
||||
export interface WatchlistRequest {
|
||||
name?: string
|
||||
domains: string[],
|
||||
triggers: { event: EventAction, action: TriggerAction }[],
|
||||
connector?: string
|
||||
dsn?: string[]
|
||||
}
|
||||
|
||||
export interface Watchlist {
|
||||
token: string
|
||||
name?: string
|
||||
domains: Domain[],
|
||||
triggers: { event: EventAction, action: TriggerAction }[],
|
||||
connector?: string
|
||||
createdAt: string
|
||||
dsn?: string[]
|
||||
}
|
||||
|
||||
export interface InstanceConfig {
|
||||
@@ -77,6 +91,16 @@ export interface InstanceConfig {
|
||||
registerEnabled: boolean
|
||||
}
|
||||
|
||||
export interface Statistics {
|
||||
rdapQueries: number
|
||||
alertSent: number
|
||||
domainPurchased: number
|
||||
domainPurchaseFailed: number
|
||||
domainCount: {tld: string, domain: number}[]
|
||||
domainCountTotal: number
|
||||
domainTracked: number
|
||||
}
|
||||
|
||||
export async function request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig): Promise<R> {
|
||||
const axiosConfig: AxiosRequestConfig = {
|
||||
...config,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {InstanceConfig, request, User} from "./index";
|
||||
import {InstanceConfig, request, Statistics, User} from "./index";
|
||||
|
||||
|
||||
export async function login(email: string, password: string): Promise<boolean> {
|
||||
@@ -32,4 +32,11 @@ export async function getConfiguration(): Promise<InstanceConfig> {
|
||||
url: 'config'
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function getStatistics(): Promise<Statistics> {
|
||||
const response = await request<Statistics>({
|
||||
url: 'stats'
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Event, request, Watchlist} from "./index";
|
||||
import {Domain, request, Watchlist, WatchlistRequest} from "./index";
|
||||
|
||||
export async function getWatchlists() {
|
||||
const response = await request({
|
||||
@@ -8,13 +8,13 @@ export async function getWatchlists() {
|
||||
}
|
||||
|
||||
export async function getWatchlist(token: string) {
|
||||
const response = await request<Watchlist & { token: string }>({
|
||||
const response = await request<Watchlist>({
|
||||
url: 'watchlists/' + token
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function postWatchlist(watchlist: Watchlist) {
|
||||
export async function postWatchlist(watchlist: WatchlistRequest) {
|
||||
const response = await request<{ token: string }>({
|
||||
method: 'POST',
|
||||
url: 'watchlists',
|
||||
@@ -33,17 +33,21 @@ export async function deleteWatchlist(token: string): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
export async function patchWatchlist(domains: string[], triggers: Event[]) {
|
||||
const response = await request<Watchlist>({
|
||||
method: 'PATCH',
|
||||
url: 'watchlists',
|
||||
data: {
|
||||
domains,
|
||||
triggers
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": 'application/merge-patch+json'
|
||||
}
|
||||
export async function putWatchlist(watchlist: Partial<WatchlistRequest> & { token: string }) {
|
||||
const response = await request<WatchlistRequest>({
|
||||
method: 'PUT',
|
||||
url: 'watchlists/' + watchlist.token,
|
||||
data: watchlist,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTrackedDomainList(params: { page: number, itemsPerPage: number }): Promise<any> {
|
||||
const response = await request({
|
||||
method: 'GET',
|
||||
url: 'tracked',
|
||||
params
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
|
||||
11
assets/utils/functions/actionToColor.tsx
Normal file
11
assets/utils/functions/actionToColor.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import {EventAction} from "../api";
|
||||
|
||||
export const actionToColor = (a: EventAction) => a === 'registration' ? 'green' :
|
||||
a === 'reregistration' ? 'cyan' :
|
||||
a === 'expiration' ? 'red' :
|
||||
a === 'deletion' ? 'magenta' :
|
||||
a === 'transfer' ? 'orange' :
|
||||
a === 'last changed' ? 'blue' :
|
||||
a === 'registrar expiration' ? 'red' :
|
||||
a === 'reinstantiation' ? 'purple' :
|
||||
a === 'enum validation expiration' ? 'red' : 'default'
|
||||
26
assets/utils/functions/actionToIcon.tsx
Normal file
26
assets/utils/functions/actionToIcon.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import {EventAction} from "../api";
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
DeleteOutlined,
|
||||
LockOutlined,
|
||||
ReloadOutlined,
|
||||
ShareAltOutlined,
|
||||
SignatureOutlined,
|
||||
SyncOutlined,
|
||||
UnlockOutlined
|
||||
} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
|
||||
export const actionToIcon = (a: EventAction) => a === 'registration' ?
|
||||
<SignatureOutlined style={{fontSize: '16px'}}/> : a === 'expiration' ?
|
||||
<ClockCircleOutlined style={{fontSize: '16px'}}/> : a === 'transfer' ?
|
||||
<ShareAltOutlined style={{fontSize: '16px'}}/> : a === 'last changed' ?
|
||||
<SyncOutlined style={{fontSize: '16px'}}/> : a === 'deletion' ?
|
||||
<DeleteOutlined style={{fontSize: '16px'}}/> : a === 'reregistration' ?
|
||||
<ReloadOutlined style={{fontSize: '16px'}}/> : a === 'locked' ?
|
||||
<LockOutlined style={{fontSize: '16px'}}/> : a === 'unlocked' ?
|
||||
<UnlockOutlined style={{fontSize: '16px'}}/> : a === 'registrar expiration' ?
|
||||
<ClockCircleOutlined
|
||||
style={{fontSize: '16px'}}/> : a === 'enum validation expiration' ?
|
||||
<ClockCircleOutlined style={{fontSize: '16px'}}/> : a === 'reinstantiation' ?
|
||||
<ReloadOutlined style={{fontSize: '16px'}}/> : undefined
|
||||
11
assets/utils/functions/entityToName.tsx
Normal file
11
assets/utils/functions/entityToName.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import {Entity} from "../api";
|
||||
import vCard from "vcf";
|
||||
|
||||
export const entityToName = (e: { entity: Entity }): string => {
|
||||
if (e.entity.jCard.length === 0) return e.entity.handle
|
||||
|
||||
const jCard = vCard.fromJSON(e.entity.jCard)
|
||||
let name = e.entity.handle
|
||||
if (jCard.data.fn && !Array.isArray(jCard.data.fn) && jCard.data.fn.valueOf() !== '') name = jCard.data.fn.valueOf()
|
||||
return name
|
||||
}
|
||||
5
assets/utils/functions/eppStatusCodeToColor.tsx
Normal file
5
assets/utils/functions/eppStatusCodeToColor.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export const eppStatusCodeToColor = (s: string) =>
|
||||
['active', 'ok'].includes(s) ? 'green' :
|
||||
['pending delete', 'redemption period'].includes(s) ? 'red' :
|
||||
s.startsWith('client') ? 'purple' :
|
||||
s.startsWith('server') ? 'geekblue' : 'blue'
|
||||
5
assets/utils/functions/getCountryCode.tsx
Normal file
5
assets/utils/functions/getCountryCode.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export const getCountryCode = (tld: string): string => {
|
||||
const exceptions = {uk: 'gb', su: 'ru', tp: 'tl'}
|
||||
if (tld in exceptions) return exceptions[tld as keyof typeof exceptions]
|
||||
return tld.toUpperCase()
|
||||
}
|
||||
117
assets/utils/functions/rdapTranslation.ts
Normal file
117
assets/utils/functions/rdapTranslation.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import {t} from "ttag";
|
||||
|
||||
/**
|
||||
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
|
||||
*/
|
||||
export const rdapRoleTranslation = () => ({
|
||||
registrant: t`Registrant`,
|
||||
technical: t`Technical`,
|
||||
administrative: t`Administrative`,
|
||||
abuse: t`Abuse`,
|
||||
billing: t`Billing`,
|
||||
registrar: t`Registrar`,
|
||||
reseller: t`Reseller`,
|
||||
sponsor: t`Sponsor`,
|
||||
proxy: t`Proxy`,
|
||||
notifications: t`Notifications`,
|
||||
noc: t`Noc`
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
|
||||
*/
|
||||
export const rdapRoleDetailTranslation = () => ({
|
||||
registrant: t`The entity object instance is the registrant of the registration. In some registries, this is known as a maintainer.`,
|
||||
technical: t`The entity object instance is a technical contact for the registration.`,
|
||||
administrative: t`The entity object instance is an administrative contact for the registration.`,
|
||||
abuse: t`The entity object instance handles network abuse issues on behalf of the registrant of the registration.`,
|
||||
billing: t`The entity object instance handles payment and billing issues on behalf of the registrant of the registration.`,
|
||||
registrar: t`The entity object instance represents the authority responsible for the registration in the registry.`,
|
||||
reseller: t`The entity object instance represents a third party through which the registration was conducted (i.e., not the registry or registrar).`,
|
||||
sponsor: t`The entity object instance represents a domain policy sponsor, such as an ICANN-approved sponsor.`,
|
||||
proxy: t`The entity object instance represents a proxy for another entity object, such as a registrant.`,
|
||||
notifications: t`An entity object instance designated to receive notifications about association object instances.`,
|
||||
noc: t`The entity object instance handles communications related to a network operations center (NOC).`
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
|
||||
*/
|
||||
export const rdapEventNameTranslation = () => ({
|
||||
registration: t`Registration`,
|
||||
reregistration: t`Reregistration`,
|
||||
'last changed': t`Changed`,
|
||||
expiration: t`Expiration`,
|
||||
deletion: t`Deletion`,
|
||||
reinstantiation: t`Reinstantiation`,
|
||||
transfer: t`Transfer`,
|
||||
locked: t`Locked`,
|
||||
unlocked: t`Unlocked`,
|
||||
'registrar expiration': t`Registrar expiration`,
|
||||
'enum validation expiration': t`ENUM validation expiration`
|
||||
})
|
||||
|
||||
/**
|
||||
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
|
||||
*/
|
||||
export const rdapEventDetailTranslation = () => ({
|
||||
registration: t`The object instance was initially registered.`,
|
||||
reregistration: t`The object instance was registered subsequently to initial registration.`,
|
||||
'last changed': t`An action noting when the information in the object instance was last changed.`,
|
||||
expiration: t`The object instance has been removed or will be removed at a predetermined date and time from the registry.`,
|
||||
deletion: t`The object instance was removed from the registry at a point in time that was not predetermined.`,
|
||||
reinstantiation: t`The object instance was reregistered after having been removed from the registry.`,
|
||||
transfer: t`The object instance was transferred from one registrar to another.`,
|
||||
locked: t`The object instance was locked.`,
|
||||
unlocked: t`The object instance was unlocked.`,
|
||||
'registrar expiration': t`An action noting the expiration date of the object in the registrar system.`,
|
||||
'enum validation expiration': t`Association of phone number represented by this ENUM domain to registrant has expired or will expire at a predetermined date and time.`
|
||||
})
|
||||
|
||||
/**
|
||||
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
|
||||
* @see https://www.icann.org/resources/pages/epp-status-codes-2014-06-16-en
|
||||
*/
|
||||
export const rdapStatusCodeDetailTranslation = () => ({
|
||||
'validated': t`Signifies that the data of the object instance has been found to be accurate.`,
|
||||
'renew prohibited': t`Renewal or reregistration of the object instance is forbidden.`,
|
||||
'update prohibited': t`Updates to the object instance are forbidden.`,
|
||||
'transfer prohibited': t`Transfers of the registration from one registrar to another are forbidden.`,
|
||||
'delete prohibited': t`Deletion of the registration of the object instance is forbidden.`,
|
||||
'proxy': t`The registration of the object instance has been performed by a third party.`,
|
||||
'private': t`The information of the object instance is not designated for public consumption.`,
|
||||
'removed': t`Some of the information of the object instance has not been made available and has been removed.`,
|
||||
'obscured': t`Some of the information of the object instance has been altered for the purposes of not readily revealing the actual information of the object instance.`,
|
||||
'associated': t`The object instance is associated with other object instances in the registry.`,
|
||||
'locked': t`Changes to the object instance cannot be made, including the association of other object instances.`,
|
||||
|
||||
'active': t`This is the standard status for a domain, meaning it has no pending operations or prohibitions.`,
|
||||
'inactive': t`This status code indicates that delegation information (name servers) has not been associated with your domain. Your domain is not activated in the DNS and will not resolve.`,
|
||||
'pending create': t`This status code indicates that a request to create your domain has been received and is being processed.`,
|
||||
'pending renew': t`This status code indicates that a request to renew your domain has been received and is being processed.`,
|
||||
'pending transfer': t`This status code indicates that a request to transfer your domain to a new registrar has been received and is being processed.`,
|
||||
'pending update': t`This status code indicates that a request to update your domain has been received and is being processed.`,
|
||||
'pending delete': t`This status code may be mixed with redemptionPeriod or pendingRestore. In such case, depending on the status (i.e. redemptionPeriod or pendingRestore) set in the domain name, the corresponding description presented above applies. If this status is not combined with the redemptionPeriod or pendingRestore status, the pendingDelete status code indicates that your domain has been in redemptionPeriod status for 30 days and you have not restored it within that 30-day period. Your domain will remain in this status for several days, after which time your domain will be purged and dropped from the registry database. Once deletion occurs, the domain is available for re-registration in accordance with the registry's policies.`,
|
||||
'add period': t`This grace period is provided after the initial registration of a domain name. If the registrar deletes the domain name during this period, the registry may provide credit to the registrar for the cost of the registration.`,
|
||||
'auto renew period': t`This grace period is provided after a domain name registration period expires and is extended (renewed) automatically by the registry. If the registrar deletes the domain name during this period, the registry provides a credit to the registrar for the cost of the renewal.`,
|
||||
'ok': t`This is the standard status for a domain, meaning it has no pending operations or prohibitions.`,
|
||||
'client delete prohibited': t`This status code tells your domain's registry to reject requests to delete the domain.`,
|
||||
'client hold': t`This status code tells your domain's registry to not activate your domain in the DNS and as a consequence, it will not resolve. It is an uncommon status that is usually enacted during legal disputes, non-payment, or when your domain is subject to deletion.`,
|
||||
'client renew prohibited': t`This status code tells your domain's registry to reject requests to renew your domain. It is an uncommon status that is usually enacted during legal disputes or when your domain is subject to deletion.`,
|
||||
'client transfer prohibited': t`This status code tells your domain's registry to reject requests to transfer the domain from your current registrar to another.`,
|
||||
'client update prohibited': t`This status code tells your domain's registry to reject requests to update the domain.`,
|
||||
'pending restore': t`This status code indicates that your registrar has asked the registry to restore your domain that was in redemptionPeriod status. Your registry will hold the domain in this status while waiting for your registrar to provide required restoration documentation. If your registrar fails to provide documentation to the registry operator within a set time period to confirm the restoration request, the domain will revert to redemptionPeriod status.`,
|
||||
'redemption period': t`This status code indicates that your registrar has asked the registry to delete your domain. Your domain will be held in this status for 30 days. After five calendar days following the end of the redemptionPeriod, your domain is purged from the registry database and becomes available for registration.`,
|
||||
'renew period': t`This grace period is provided after a domain name registration period is explicitly extended (renewed) by the registrar. If the registrar deletes the domain name during this period, the registry provides a credit to the registrar for the cost of the renewal.`,
|
||||
'server delete prohibited': t`This status code prevents your domain from being deleted. It is an uncommon status that is usually enacted during legal disputes, at your request, or when a redemptionPeriod status is in place.`,
|
||||
'server renew prohibited': t`This status code indicates your domain's Registry Operator will not allow your registrar to renew your domain. It is an uncommon status that is usually enacted during legal disputes or when your domain is subject to deletion.`,
|
||||
'server transfer prohibited': t`This status code prevents your domain from being transferred from your current registrar to another. It is an uncommon status that is usually enacted during legal or other disputes, at your request, or when a redemptionPeriod status is in place.`,
|
||||
'server update prohibited': t`This status code locks your domain preventing it from being updated. It is an uncommon status that is usually enacted during legal disputes, at your request, or when a redemptionPeriod status is in place.`,
|
||||
'server hold': t`This status code is set by your domain's Registry Operator. Your domain is not activated in the DNS.`,
|
||||
'transfer period': t`This grace period is provided after the successful transfer of a domain name from one registrar to another. If the new registrar deletes the domain name during this period, the registry provides a credit to the registrar for the cost of the transfer.`,
|
||||
|
||||
'administrative': t`The object instance has been allocated administratively (i.e., not for use by the recipient in their own right in operational networks).`,
|
||||
'reserved': t`The object instance has been allocated to an IANA special-purpose address registry.`,
|
||||
})
|
||||
24
assets/utils/functions/roleToAvatar.tsx
Normal file
24
assets/utils/functions/roleToAvatar.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import {Avatar} from "antd";
|
||||
import {
|
||||
BankOutlined,
|
||||
DollarOutlined,
|
||||
IdcardOutlined,
|
||||
SignatureOutlined,
|
||||
ToolOutlined,
|
||||
UserOutlined
|
||||
} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
|
||||
import {rolesToColor} from "./rolesToColor";
|
||||
|
||||
export const roleToAvatar = (e: { roles: string[] }) => <Avatar style={{backgroundColor: rolesToColor(e.roles)}}
|
||||
icon={e.roles.includes('registrant') ?
|
||||
<SignatureOutlined/> : e.roles.includes('registrar') ?
|
||||
<BankOutlined/> :
|
||||
e.roles.includes('technical') ?
|
||||
<ToolOutlined/> :
|
||||
e.roles.includes('administrative') ?
|
||||
<IdcardOutlined/> :
|
||||
e.roles.includes('billing') ?
|
||||
<DollarOutlined/> :
|
||||
<UserOutlined/>}/>
|
||||
6
assets/utils/functions/rolesToColor.tsx
Normal file
6
assets/utils/functions/rolesToColor.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
export const rolesToColor = (roles: string[]) => roles.includes('registrant') ? 'green' :
|
||||
roles.includes('technical') ? 'orange' :
|
||||
roles.includes('administrative') ? 'blue' :
|
||||
roles.includes('registrar') ? 'purple' :
|
||||
roles.includes('sponsor') ? 'magenta' :
|
||||
roles.includes('billing') ? 'cyan' : 'default'
|
||||
@@ -1,5 +1,5 @@
|
||||
import {MessageInstance, MessageType} from "antd/lib/message/interface";
|
||||
import {AxiosError, AxiosResponse} from "axios";
|
||||
import {MessageInstance, MessageType} from "antd/lib/message/interface";
|
||||
import {t} from "ttag";
|
||||
|
||||
export function showErrorAPI(e: AxiosError, messageApi: MessageInstance): MessageType | undefined {
|
||||
11
assets/utils/functions/sortDomainEntities.tsx
Normal file
11
assets/utils/functions/sortDomainEntities.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import {Domain} from "../api";
|
||||
|
||||
export const sortDomainEntities = (domain: Domain) => domain.entities
|
||||
.filter(e => !e.deleted)
|
||||
.sort((e1, e2) => {
|
||||
const p = (r: string[]) => r.includes('registrant') ? 5 :
|
||||
r.includes('administrative') ? 4 :
|
||||
r.includes('billing') ? 3 :
|
||||
r.includes('registrar') ? 2 : 1
|
||||
return p(e2.roles) - p(e1.roles)
|
||||
})
|
||||
12
assets/utils/functions/tldToEmoji.tsx
Normal file
12
assets/utils/functions/tldToEmoji.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import {getCountryCode} from "./getCountryCode";
|
||||
|
||||
export const tldToEmoji = (tld: string) => {
|
||||
if (tld.startsWith('xn--')) return '-'
|
||||
|
||||
return String.fromCodePoint(
|
||||
...getCountryCode(tld)
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map((char) => 127397 + char.charCodeAt(0))
|
||||
)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export const helpGetTokenLink = (provider?: string) => {
|
||||
switch (provider) {
|
||||
case ConnectorProvider.OVH:
|
||||
return <Typography.Link target='_blank'
|
||||
href="https://api.ovh.com/createToken/index.cgi?GET=/order/cart&GET=/order/cart/*&POST=/order/cart&POST=/order/cart/*&DELETE=/order/cart/*">
|
||||
href="https://api.ovh.com/createToken/?GET=/order/cart&GET=/order/cart/*&POST=/order/cart&POST=/order/cart/*&DELETE=/order/cart/*&GET=/domain/extensions">
|
||||
{t`Retrieve a set of tokens from your customer account on the Provider's website`}
|
||||
</Typography.Link>
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database:
|
||||
ports:
|
||||
- "5432"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
mailer:
|
||||
image: axllent/mailpit
|
||||
ports:
|
||||
- "1025"
|
||||
- "8025"
|
||||
environment:
|
||||
MP_SMTP_AUTH_ACCEPT_ANY: 1
|
||||
MP_SMTP_AUTH_ALLOW_INSECURE: 1
|
||||
###< symfony/mailer ###
|
||||
26
compose.yaml
26
compose.yaml
@@ -1,26 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database:
|
||||
image: postgres:${POSTGRES_VERSION:-16}-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-app}
|
||||
# You should definitely change the password in production
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-app}
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"]
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
volumes:
|
||||
- database_data:/var/lib/postgresql/data:rw
|
||||
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
|
||||
# - ./docker/db/data:/var/lib/postgresql/data:rw
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
volumes:
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database_data:
|
||||
###< doctrine/doctrine-bundle ###
|
||||
@@ -37,32 +37,45 @@
|
||||
"phpstan/phpdoc-parser": "^1.29",
|
||||
"protonlabs/vobject": "^4.31",
|
||||
"psr/http-client": "^1.0",
|
||||
"runtime/frankenphp-symfony": "^0.2.0",
|
||||
"symfony/asset": "7.1.*",
|
||||
"symfony/asset-mapper": "7.1.*",
|
||||
"symfony/cache": "7.1.*",
|
||||
"symfony/console": "7.1.*",
|
||||
"symfony/discord-notifier": "7.1.*",
|
||||
"symfony/doctrine-messenger": "7.1.*",
|
||||
"symfony/dotenv": "7.1.*",
|
||||
"symfony/engagespot-notifier": "7.1.*",
|
||||
"symfony/expression-language": "7.1.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/form": "7.1.*",
|
||||
"symfony/framework-bundle": "7.1.*",
|
||||
"symfony/google-chat-notifier": "7.1.*",
|
||||
"symfony/http-client": "7.1.*",
|
||||
"symfony/intl": "7.1.*",
|
||||
"symfony/lock": "7.1.*",
|
||||
"symfony/mailer": "7.1.*",
|
||||
"symfony/mattermost-notifier": "7.1.*",
|
||||
"symfony/microsoft-teams-notifier": "7.1.*",
|
||||
"symfony/mime": "7.1.*",
|
||||
"symfony/monolog-bundle": "^3.0",
|
||||
"symfony/notifier": "7.1.*",
|
||||
"symfony/ntfy-notifier": "7.1.*",
|
||||
"symfony/process": "7.1.*",
|
||||
"symfony/property-access": "7.1.*",
|
||||
"symfony/property-info": "7.1.*",
|
||||
"symfony/pushover-notifier": "7.1.*",
|
||||
"symfony/rate-limiter": "7.1.*",
|
||||
"symfony/redis-messenger": "7.1.*",
|
||||
"symfony/rocket-chat-notifier": "7.1.*",
|
||||
"symfony/runtime": "7.1.*",
|
||||
"symfony/scheduler": "7.1.*",
|
||||
"symfony/security-bundle": "7.1.*",
|
||||
"symfony/serializer": "7.1.*",
|
||||
"symfony/slack-notifier": "7.1.*",
|
||||
"symfony/stimulus-bundle": "^2.18",
|
||||
"symfony/string": "7.1.*",
|
||||
"symfony/telegram-notifier": "7.1.*",
|
||||
"symfony/translation": "7.1.*",
|
||||
"symfony/twig-bundle": "7.1.*",
|
||||
"symfony/uid": "7.1.*",
|
||||
@@ -71,6 +84,7 @@
|
||||
"symfony/web-link": "7.1.*",
|
||||
"symfony/webpack-encore-bundle": "^2.1",
|
||||
"symfony/yaml": "7.1.*",
|
||||
"symfony/zulip-notifier": "7.1.*",
|
||||
"symfonycasts/verify-email-bundle": "*",
|
||||
"twig/extra-bundle": "^2.12|^3.0",
|
||||
"twig/twig": "^2.12|^3.0",
|
||||
|
||||
883
composer.lock
generated
883
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "69d672f9e5a01b48f871fa5c81714f8d",
|
||||
"content-hash": "1983a126aba2f9d83d15ac3d08b79418",
|
||||
"packages": [
|
||||
{
|
||||
"name": "api-platform/core",
|
||||
@@ -3434,6 +3434,58 @@
|
||||
},
|
||||
"time": "2019-03-08T08:55:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "runtime/frankenphp-symfony",
|
||||
"version": "0.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-runtime/frankenphp-symfony.git",
|
||||
"reference": "56822c3631d9522a3136a4c33082d006bdfe4bad"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-runtime/frankenphp-symfony/zipball/56822c3631d9522a3136a4c33082d006bdfe4bad",
|
||||
"reference": "56822c3631d9522a3136a4c33082d006bdfe4bad",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0",
|
||||
"symfony/http-kernel": "^5.4 || ^6.0 || ^7.0",
|
||||
"symfony/runtime": "^5.4 || ^6.0 || ^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Runtime\\FrankenPhpSymfony\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Kévin Dunglas",
|
||||
"email": "kevin@dunglas.dev"
|
||||
}
|
||||
],
|
||||
"description": "FrankenPHP runtime for Symfony",
|
||||
"support": {
|
||||
"issues": "https://github.com/php-runtime/frankenphp-symfony/issues",
|
||||
"source": "https://github.com/php-runtime/frankenphp-symfony/tree/0.2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/nyholm",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-12-12T12:06:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sabre/uri",
|
||||
"version": "3.0.1",
|
||||
@@ -3713,16 +3765,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/cache",
|
||||
"version": "v7.1.2",
|
||||
"version": "v7.1.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/cache.git",
|
||||
"reference": "e933e1d947ffb88efcdd34a2bd51561cab7deaae"
|
||||
"reference": "8ac37acee794372f9732fe8a61a8221f6762148e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/cache/zipball/e933e1d947ffb88efcdd34a2bd51561cab7deaae",
|
||||
"reference": "e933e1d947ffb88efcdd34a2bd51561cab7deaae",
|
||||
"url": "https://api.github.com/repos/symfony/cache/zipball/8ac37acee794372f9732fe8a61a8221f6762148e",
|
||||
"reference": "8ac37acee794372f9732fe8a61a8221f6762148e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3790,7 +3842,7 @@
|
||||
"psr6"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/cache/tree/v7.1.2"
|
||||
"source": "https://github.com/symfony/cache/tree/v7.1.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3806,7 +3858,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-11T13:32:38+00:00"
|
||||
"time": "2024-07-17T06:10:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/cache-contracts",
|
||||
@@ -4273,6 +4325,74 @@
|
||||
],
|
||||
"time": "2024-04-18T09:32:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/discord-notifier",
|
||||
"version": "v7.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/discord-notifier.git",
|
||||
"reference": "f3d8368ca5ff80c1268a851f925e1f0c07997a8e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/discord-notifier/zipball/f3d8368ca5ff80c1268a851f925e1f0c07997a8e",
|
||||
"reference": "f3d8368ca5ff80c1268a851f925e1f0c07997a8e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/notifier": "^6.4|^7.0",
|
||||
"symfony/polyfill-mbstring": "^1.0"
|
||||
},
|
||||
"type": "symfony-notifier-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Notifier\\Bridge\\Discord\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Discord Notifier Bridge",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"discord",
|
||||
"notifier"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/discord-notifier/tree/v7.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/doctrine-bridge",
|
||||
"version": "v7.1.2",
|
||||
@@ -4527,6 +4647,74 @@
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/engagespot-notifier",
|
||||
"version": "v7.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/engagespot-notifier.git",
|
||||
"reference": "e1beca155abf4ecc838eb9093b82fd7c8c01383f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/engagespot-notifier/zipball/e1beca155abf4ecc838eb9093b82fd7c8c01383f",
|
||||
"reference": "e1beca155abf4ecc838eb9093b82fd7c8c01383f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/notifier": "^6.4|^7.0"
|
||||
},
|
||||
"type": "symfony-notifier-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Notifier\\Bridge\\Engagespot\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Daniel Gorgan",
|
||||
"homepage": "https://github.com/danut007ro"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Engagespot Notifier Bridge",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"engagespot",
|
||||
"notifier",
|
||||
"push"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/engagespot-notifier/tree/v7.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/error-handler",
|
||||
"version": "v7.1.2",
|
||||
@@ -5261,6 +5449,75 @@
|
||||
],
|
||||
"time": "2024-06-28T08:00:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/google-chat-notifier",
|
||||
"version": "v7.1.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/google-chat-notifier.git",
|
||||
"reference": "1e92b6c89b2182ba26554861dc261c530c98000f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/google-chat-notifier/zipball/1e92b6c89b2182ba26554861dc261c530c98000f",
|
||||
"reference": "1e92b6c89b2182ba26554861dc261c530c98000f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/notifier": "^6.4|^7.0"
|
||||
},
|
||||
"type": "symfony-notifier-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Notifier\\Bridge\\GoogleChat\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Google Chat Notifier Bridge",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"Google-Chat",
|
||||
"chat",
|
||||
"google",
|
||||
"notifier"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/google-chat-notifier/tree/v7.1.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-25T19:55:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client",
|
||||
"version": "v7.1.2",
|
||||
@@ -5868,6 +6125,73 @@
|
||||
],
|
||||
"time": "2024-06-28T08:00:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/mattermost-notifier",
|
||||
"version": "v7.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/mattermost-notifier.git",
|
||||
"reference": "c5ff6774682ab3504a77bbe01f8c1275b4bf48e9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/mattermost-notifier/zipball/c5ff6774682ab3504a77bbe01f8c1275b4bf48e9",
|
||||
"reference": "c5ff6774682ab3504a77bbe01f8c1275b4bf48e9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/notifier": "^6.4|^7.0"
|
||||
},
|
||||
"type": "symfony-notifier-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Notifier\\Bridge\\Mattermost\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Emanuele Panzeri",
|
||||
"email": "thepanz@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Mattermost Notifier Bridge",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"Mattermost",
|
||||
"notifier"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/mattermost-notifier/tree/v7.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/messenger",
|
||||
"version": "v7.1.2",
|
||||
@@ -5954,6 +6278,78 @@
|
||||
],
|
||||
"time": "2024-06-28T08:00:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/microsoft-teams-notifier",
|
||||
"version": "v7.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/microsoft-teams-notifier.git",
|
||||
"reference": "546b0368928b5849d08728b7daf5d22a07a052b3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/microsoft-teams-notifier/zipball/546b0368928b5849d08728b7daf5d22a07a052b3",
|
||||
"reference": "546b0368928b5849d08728b7daf5d22a07a052b3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/notifier": "^6.4|^7.0"
|
||||
},
|
||||
"type": "symfony-notifier-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Notifier\\Bridge\\MicrosoftTeams\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Edouard Lescot",
|
||||
"email": "edouard.lescot@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Oskar Stark",
|
||||
"email": "oskarstark@googlemail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Microsoft Teams Notifier Bridge",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"chat",
|
||||
"microsoft-teams",
|
||||
"notifier"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/microsoft-teams-notifier/tree/v7.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/mime",
|
||||
"version": "v7.1.2",
|
||||
@@ -6275,6 +6671,73 @@
|
||||
],
|
||||
"time": "2024-06-28T08:00:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/ntfy-notifier",
|
||||
"version": "v7.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/ntfy-notifier.git",
|
||||
"reference": "9fa7e4991eb3f6879e4afdeb1a0112daac09d320"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/ntfy-notifier/zipball/9fa7e4991eb3f6879e4afdeb1a0112daac09d320",
|
||||
"reference": "9fa7e4991eb3f6879e4afdeb1a0112daac09d320",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/notifier": "^6.4|^7.0"
|
||||
},
|
||||
"type": "symfony-notifier-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Notifier\\Bridge\\Ntfy\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mickael Perraud",
|
||||
"email": "mikaelkael.fr@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Ntyf Notifier Bridge",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"Ntfy",
|
||||
"notifier"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/ntfy-notifier/tree/v7.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
"version": "v7.1.1",
|
||||
@@ -7197,6 +7660,76 @@
|
||||
],
|
||||
"time": "2024-06-26T07:21:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/pushover-notifier",
|
||||
"version": "v7.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/pushover-notifier.git",
|
||||
"reference": "76072ef5c5230e201f1ae83229b2d1b1a6455ede"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/pushover-notifier/zipball/76072ef5c5230e201f1ae83229b2d1b1a6455ede",
|
||||
"reference": "76072ef5c5230e201f1ae83229b2d1b1a6455ede",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/notifier": "^6.4|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/event-dispatcher": "^6.4|^7.0"
|
||||
},
|
||||
"type": "symfony-notifier-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Notifier\\Bridge\\Pushover\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "mocodo",
|
||||
"homepage": "https://github.com/mocodo"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Pushover Notifier Bridge",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"notifier",
|
||||
"pushover"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/pushover-notifier/tree/v7.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/rate-limiter",
|
||||
"version": "v7.1.1",
|
||||
@@ -7267,6 +7800,140 @@
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/redis-messenger",
|
||||
"version": "v7.1.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/redis-messenger.git",
|
||||
"reference": "0e13be260a411afbe14f77df45728a23ffb50e7d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/redis-messenger/zipball/0e13be260a411afbe14f77df45728a23ffb50e7d",
|
||||
"reference": "0e13be260a411afbe14f77df45728a23ffb50e7d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-redis": "*",
|
||||
"php": ">=8.2",
|
||||
"symfony/messenger": "^6.4|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/property-access": "^6.4|^7.0",
|
||||
"symfony/serializer": "^6.4|^7.0"
|
||||
},
|
||||
"type": "symfony-messenger-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Messenger\\Bridge\\Redis\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Redis extension Messenger Bridge",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/redis-messenger/tree/v7.1.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-17T06:10:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/rocket-chat-notifier",
|
||||
"version": "v7.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/rocket-chat-notifier.git",
|
||||
"reference": "b17bff59107b51753e3e347c5194dc304019daf7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/rocket-chat-notifier/zipball/b17bff59107b51753e3e347c5194dc304019daf7",
|
||||
"reference": "b17bff59107b51753e3e347c5194dc304019daf7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/notifier": "^6.4|^7.0"
|
||||
},
|
||||
"type": "symfony-notifier-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Notifier\\Bridge\\RocketChat\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jeroen Spee",
|
||||
"homepage": "https://github.com/Jeroeny"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony RocketChat Notifier Bridge",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"notifier",
|
||||
"rocketchat"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/rocket-chat-notifier/tree/v7.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/routing",
|
||||
"version": "v7.1.1",
|
||||
@@ -8035,6 +8702,73 @@
|
||||
],
|
||||
"time": "2024-04-18T09:32:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/slack-notifier",
|
||||
"version": "v7.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/slack-notifier.git",
|
||||
"reference": "452a17e3935192e6a9a5b16f0443911d67e456af"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/slack-notifier/zipball/452a17e3935192e6a9a5b16f0443911d67e456af",
|
||||
"reference": "452a17e3935192e6a9a5b16f0443911d67e456af",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/notifier": "^6.4|^7.0"
|
||||
},
|
||||
"type": "symfony-notifier-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Notifier\\Bridge\\Slack\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Slack Notifier Bridge",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"notifier",
|
||||
"slack"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/slack-notifier/tree/v7.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/stimulus-bundle",
|
||||
"version": "v2.18.1",
|
||||
@@ -8253,6 +8987,74 @@
|
||||
],
|
||||
"time": "2024-06-28T09:27:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/telegram-notifier",
|
||||
"version": "v7.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/telegram-notifier.git",
|
||||
"reference": "521e77470d5b07306c1001c2d1d1bc88474a8035"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/telegram-notifier/zipball/521e77470d5b07306c1001c2d1d1bc88474a8035",
|
||||
"reference": "521e77470d5b07306c1001c2d1d1bc88474a8035",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/mime": "^6.4|^7.0",
|
||||
"symfony/notifier": "^6.4|^7.0"
|
||||
},
|
||||
"type": "symfony-notifier-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Notifier\\Bridge\\Telegram\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Telegram Notifier Bridge",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"notifier",
|
||||
"telegram"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/telegram-notifier/tree/v7.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/translation",
|
||||
"version": "v7.1.1",
|
||||
@@ -9352,6 +10154,73 @@
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/zulip-notifier",
|
||||
"version": "v7.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/zulip-notifier.git",
|
||||
"reference": "48b3e1ac791d8eac7ee268108865b36de3be5ed2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/zulip-notifier/zipball/48b3e1ac791d8eac7ee268108865b36de3be5ed2",
|
||||
"reference": "48b3e1ac791d8eac7ee268108865b36de3be5ed2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/notifier": "^6.4|^7.0"
|
||||
},
|
||||
"type": "symfony-notifier-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Notifier\\Bridge\\Zulip\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mohammad Emran Hasan",
|
||||
"email": "phpfour@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Zulip Notifier Bridge",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"notifier",
|
||||
"zulip"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/zulip-notifier/tree/v7.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfonycasts/verify-email-bundle",
|
||||
"version": "v1.17.0",
|
||||
|
||||
1
config/app/.gitignore
vendored
Normal file
1
config/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/custom_rdap_servers.yaml
|
||||
63
config/app/custom_rdap_servers.example.yaml
Normal file
63
config/app/custom_rdap_servers.example.yaml
Normal file
@@ -0,0 +1,63 @@
|
||||
#############################################################
|
||||
# WARNING #
|
||||
# #
|
||||
# This list of RDAP servers is not published by IANA #
|
||||
# !!! Use it at your own risk !!! #
|
||||
# #
|
||||
# This file must comply with RFC 9224 #
|
||||
#############################################################
|
||||
|
||||
# Feel free to contribute to add missing RDAP servers to this file.
|
||||
# To generate the list of TLDs that do not have an RDAP server,
|
||||
# run this SQL query on a Domain Watchdog instance based on the official IANA list
|
||||
# SELECT t.tld FROM tld t LEFT JOIN rdap_server rs ON rs.tld_id = t.tld WHERE t.contract_terminated IS NOT TRUE and rs.url IS NULL;
|
||||
|
||||
{
|
||||
"description": "Custom RDAP bootstrap file for Domain Name System registrations",
|
||||
|
||||
# Remove the following line to use servers from this list as a priority
|
||||
# Otherwise, those from the official IANA list will have a preponderant value
|
||||
"publication": "1970-01-01T00:00:00Z",
|
||||
|
||||
"services": [
|
||||
[ [ "ad" ], [ "https://rdap.nic.ad/" ] ],
|
||||
[ [ "ae" ], [ "https://rdap.nic.ae/" ] ],
|
||||
[ [ "ki" ], [ "https://rdap.nic.ki/" ] ],
|
||||
[ [ "af" ], [ "https://rdap.nic.af/" ] ],
|
||||
[
|
||||
[
|
||||
"ag",
|
||||
"me",
|
||||
"bz",
|
||||
"gi",
|
||||
"pr",
|
||||
"sc",
|
||||
"vc"
|
||||
],
|
||||
[
|
||||
"https://rdap.identitydigital.services/rdap/"
|
||||
]
|
||||
],
|
||||
[ [ "ch" ], [ "https://rdap.nic.ch/" ] ],
|
||||
[ [ "co" ], [ "https://rdap.nic.co/" ] ],
|
||||
[ [ "de" ], [ "https://rdap.denic.de/" ] ],
|
||||
[ [ "ga" ], [ "https://rdap.nic.ga/" ] ],
|
||||
[ [ "ht" ], [ "https://rdap.nic.ht/" ] ],
|
||||
[ [ "in" ], [ "https://rdap.registry.in/" ] ],
|
||||
[ [ "kn" ], [ "https://rdap.nic.kn/" ] ],
|
||||
[ [ "li" ], [ "https://rdap.nic.li/" ] ],
|
||||
[ [ "ml" ], [ "https://rdap.nic.ml/" ] ],
|
||||
[ [ "mr" ], [ "https://rdap.nic.mr/" ] ],
|
||||
[ [ "mz" ], [ "https://rdap.nic.mz/" ] ],
|
||||
[ [ "pr" ], [ "https://rdap.afilias-srs.net/rdap/pr/" ] ],
|
||||
[ [ "sb" ], [ "https://rdap.nic.sb/" ] ],
|
||||
[ [ "sn" ], [ "https://rdap.nic.sn/" ] ],
|
||||
[ [ "so" ], [ "https://rdap.nic.so/" ] ],
|
||||
[ [ "td" ], [ "https://rdap.nic.td/" ] ],
|
||||
[ [ "tl" ], [ "https://rdap.nic.tl/" ] ],
|
||||
[ [ "us" ], [ "https://rdap.nic.us/" ] ],
|
||||
[ [ "ve" ], [ "https://rdap.nic.ve/rdap/" ] ],
|
||||
[ [ "ws" ], [ "https://rdap.website.ws/" ] ],
|
||||
],
|
||||
"version": "1.0"
|
||||
}
|
||||
@@ -2,7 +2,7 @@ lexik_jwt_authentication:
|
||||
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
|
||||
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
|
||||
pass_phrase: '%env(JWT_PASSPHRASE)%'
|
||||
token_ttl: 7200 # in seconds, default is 3600
|
||||
token_ttl: 604800 # in seconds, default is 3600
|
||||
token_extractors:
|
||||
authorization_header:
|
||||
enabled: true
|
||||
|
||||
@@ -6,9 +6,6 @@ framework:
|
||||
# https://symfony.com/doc/current/messenger.html#transport-configuration
|
||||
async:
|
||||
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
|
||||
options:
|
||||
use_notify: true
|
||||
check_delayed_interval: 60000
|
||||
retry_strategy:
|
||||
max_retries: 3
|
||||
multiplier: 2
|
||||
@@ -18,16 +15,18 @@ framework:
|
||||
default_bus: messenger.bus.default
|
||||
|
||||
buses:
|
||||
messenger.bus.default: []
|
||||
messenger.bus.default: [ ]
|
||||
|
||||
routing:
|
||||
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
|
||||
Symfony\Component\Notifier\Message\ChatMessage: async
|
||||
Symfony\Component\Notifier\Message\SmsMessage: async
|
||||
App\Message\UpdateRdapServers: async
|
||||
|
||||
App\Message\OrderDomain: async
|
||||
App\Message\ProcessWatchListsTrigger: async
|
||||
App\Message\ProcessWatchListTrigger: async
|
||||
App\Message\ProcessDomainTrigger: async
|
||||
App\Message\SendDomainEventNotif: async
|
||||
App\Message\UpdateDomainsFromWatchlist: async
|
||||
App\Message\UpdateRdapServers: async
|
||||
|
||||
# Route your messages to the transports
|
||||
# 'App\Message\YourMessage': async
|
||||
|
||||
@@ -9,7 +9,7 @@ when@dev:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event"]
|
||||
channels: [ "!event" ]
|
||||
# uncomment to get logging in your browser
|
||||
# you may have to allow bigger header sizes in your Web server configuration
|
||||
#firephp:
|
||||
@@ -21,7 +21,7 @@ when@dev:
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
||||
channels: [ "!event", "!doctrine", "!console" ]
|
||||
|
||||
when@test:
|
||||
monolog:
|
||||
@@ -30,8 +30,8 @@ when@test:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
channels: ["!event"]
|
||||
excluded_http_codes: [ 404, 405 ]
|
||||
channels: [ "!event" ]
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
@@ -42,9 +42,9 @@ when@prod:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
action_level: notice
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
excluded_http_codes: [ 404, 405 ]
|
||||
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||
nested:
|
||||
type: stream
|
||||
@@ -54,9 +54,9 @@ when@prod:
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine"]
|
||||
channels: [ "!event", "!doctrine" ]
|
||||
deprecation:
|
||||
type: stream
|
||||
channels: [deprecation]
|
||||
channels: [ deprecation ]
|
||||
path: php://stderr
|
||||
formatter: monolog.formatter.json
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
framework:
|
||||
notifier:
|
||||
chatter_transports:
|
||||
zulip: '%env(ZULIP_DSN)%'
|
||||
telegram: '%env(TELEGRAM_DSN)%'
|
||||
slack: '%env(SLACK_DSN)%'
|
||||
rocketchat: '%env(ROCKETCHAT_DSN)%'
|
||||
microsoftteams: '%env(MICROSOFT_TEAMS_DSN)%'
|
||||
mattermost: '%env(MATTERMOST_DSN)%'
|
||||
googlechat: '%env(GOOGLE_CHAT_DSN)%'
|
||||
discord: '%env(DISCORD_DSN)%'
|
||||
texter_transports:
|
||||
engagespot: '%env(ENGAGESPOT_DSN)%'
|
||||
pushover: '%env(PUSHOVER_DSN)%'
|
||||
channel_policy:
|
||||
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
|
||||
urgent: ['email']
|
||||
|
||||
@@ -4,14 +4,18 @@
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
custom_rdap_servers_file: '%kernel.project_dir%/config/app/custom_rdap_servers.yaml'
|
||||
|
||||
mailer_sender_email: '%env(string:MAILER_SENDER_EMAIL)%'
|
||||
mailer_sender_name: '%env(string:MAILER_SENDER_NAME)%'
|
||||
oauth_enabled: '%env(OAUTH_CLIENT_ID)%'
|
||||
registration_enabled: '%env(bool:REGISTRATION_ENABLED)%'
|
||||
registration_verify_email: '%env(bool:REGISTRATION_VERIFY_EMAIL)%'
|
||||
|
||||
limited_features: '%env(bool:LIMITED_FEATURES)%'
|
||||
limit_max_watchlist: '%env(int:LIMIT_MAX_WATCHLIST)%'
|
||||
limit_max_watchlist_domains: '%env(int:LIMIT_MAX_WATCHLIST_DOMAINS)%'
|
||||
limit_max_watchlist_webhooks: '%env(int:LIMIT_MAX_WATCHLIST_WEBHOOKS)%'
|
||||
|
||||
outgoing_ip: '%env(string:OUTGOING_IP)%'
|
||||
|
||||
|
||||
55
docker-compose.yml
Normal file
55
docker-compose.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
# Please see https://github.com/maelgangloff/domain-watchdog
|
||||
services:
|
||||
domainwatchdog:
|
||||
image: maelgangloff/domain-watchdog:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SERVER_NAME: ${SERVER_NAME:-:80}
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@${POSTGRES_HOST:-database}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-15}&charset=${POSTGRES_CHARSET:-utf8}
|
||||
APP_SECRET: ${APP_SECRET:-ChangeMe}
|
||||
REGISTRATION_ENABLED: ${REGISTRATION_ENABLED:-true}
|
||||
REGISTRATION_VERIFY_EMAIL: ${REGISTRATION_VERIFY_EMAIL:-false}
|
||||
LIMITED_FEATURES: ${LIMITED_FEATURES:-false}
|
||||
LIMIT_MAX_WATCHLIST: ${LIMIT_MAX_WATCHLIST:-0}
|
||||
LIMIT_MAX_WATCHLIST_DOMAINS: ${LIMIT_MAX_WATCHLIST_DOMAINS:-0}
|
||||
LIMIT_MAX_WATCHLIST_WEBHOOKS: ${LIMIT_MAX_WATCHLIST_WEBHOOKS:-0}
|
||||
MAILER_DSN: ${MAILER_DSN:-null://null}
|
||||
volumes:
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
- ./public/content:/app/public/content
|
||||
ports:
|
||||
- 127.0.0.1:8080:80
|
||||
|
||||
php-worker:
|
||||
image: maelgangloff/domain-watchdog:latest
|
||||
restart: always
|
||||
command: php /app/bin/console messenger:consume --all --time-limit=3600 -vvv
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@${POSTGRES_HOST:-database}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-15}&charset=${POSTGRES_CHARSET:-utf8}
|
||||
APP_SECRET: ${APP_SECRET:-ChangeMe}
|
||||
MAILER_DSN: ${MAILER_DSN:-null://null}
|
||||
healthcheck:
|
||||
disable: true
|
||||
test: [ ]
|
||||
# volumes:
|
||||
# - ./custom_rdap_servers.yaml:/app/config/app/custom_rdap_servers.yaml # Please see #41 issue
|
||||
|
||||
database:
|
||||
image: postgres:${POSTGRES_VERSION:-16}-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-app}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-app}
|
||||
healthcheck:
|
||||
test: [ "CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}" ]
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
volumes:
|
||||
- database_data:/var/lib/postgresql/data:rw
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
database_data:
|
||||
21
frankenphp/Caddyfile
Normal file
21
frankenphp/Caddyfile
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
{$CADDY_GLOBAL_OPTIONS}
|
||||
|
||||
frankenphp {
|
||||
{$FRANKENPHP_CONFIG}
|
||||
}
|
||||
}
|
||||
|
||||
{$CADDY_EXTRA_CONFIG}
|
||||
|
||||
{$SERVER_NAME:localhost} {
|
||||
root * /app/public
|
||||
encode zstd br gzip
|
||||
|
||||
{$CADDY_SERVER_EXTRA_DIRECTIVES}
|
||||
|
||||
# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
|
||||
header ?Permissions-Policy "browsing-topics=()"
|
||||
|
||||
php_server
|
||||
}
|
||||
13
frankenphp/conf.d/10-app.ini
Normal file
13
frankenphp/conf.d/10-app.ini
Normal file
@@ -0,0 +1,13 @@
|
||||
expose_php = 0
|
||||
date.timezone = UTC
|
||||
apc.enable_cli = 1
|
||||
session.use_strict_mode = 1
|
||||
zend.detect_unicode = 0
|
||||
|
||||
; https://symfony.com/doc/current/performance.html
|
||||
realpath_cache_size = 4096K
|
||||
realpath_cache_ttl = 600
|
||||
opcache.interned_strings_buffer = 16
|
||||
opcache.max_accelerated_files = 20000
|
||||
opcache.memory_consumption = 256
|
||||
opcache.enable_file_override = 1
|
||||
5
frankenphp/conf.d/20-app.dev.ini
Normal file
5
frankenphp/conf.d/20-app.dev.ini
Normal file
@@ -0,0 +1,5 @@
|
||||
; See https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host
|
||||
; See https://github.com/docker/for-linux/issues/264
|
||||
; The `client_host` below may optionally be replaced with `discover_client_host=yes`
|
||||
; Add `start_with_request=yes` to start debug session on each request
|
||||
xdebug.client_host = host.docker.internal
|
||||
2
frankenphp/conf.d/20-app.prod.ini
Normal file
2
frankenphp/conf.d/20-app.prod.ini
Normal file
@@ -0,0 +1,2 @@
|
||||
opcache.preload_user = root
|
||||
opcache.preload = /app/config/preload.php
|
||||
63
frankenphp/docker-entrypoint.sh
Executable file
63
frankenphp/docker-entrypoint.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then
|
||||
# Install the project the first time PHP is started
|
||||
# After the installation, the following block can be deleted
|
||||
if [ ! -f composer.json ]; then
|
||||
rm -Rf tmp/
|
||||
composer create-project "symfony/skeleton $SYMFONY_VERSION" tmp --stability="$STABILITY" --prefer-dist --no-progress --no-interaction --no-install
|
||||
|
||||
cd tmp
|
||||
cp -Rp . ..
|
||||
cd -
|
||||
rm -Rf tmp/
|
||||
|
||||
composer require "php:>=$PHP_VERSION" runtime/frankenphp-symfony
|
||||
composer config --json extra.symfony.docker 'true'
|
||||
|
||||
if grep -q ^DATABASE_URL= .env; then
|
||||
echo "To finish the installation please press Ctrl+C to stop Docker Compose and run: docker compose up --build -d --wait"
|
||||
sleep infinity
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$(ls -A 'vendor/' 2>/dev/null)" ]; then
|
||||
composer install --prefer-dist --no-progress --no-interaction
|
||||
fi
|
||||
|
||||
if grep -q ^DATABASE_URL= .env; then
|
||||
echo "Waiting for database to be ready..."
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=60
|
||||
until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do
|
||||
if [ $? -eq 255 ]; then
|
||||
# If the Doctrine command exits with 255, an unrecoverable error occurred
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=0
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1))
|
||||
echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left."
|
||||
done
|
||||
|
||||
if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then
|
||||
echo "The database is not up or not reachable:"
|
||||
echo "$DATABASE_ERROR"
|
||||
exit 1
|
||||
else
|
||||
echo "The database is now ready and reachable"
|
||||
fi
|
||||
|
||||
if [ "$( find ./migrations -iname '*.php' -print -quit )" ]; then
|
||||
php bin/console doctrine:migrations:migrate --no-interaction --all-or-nothing
|
||||
fi
|
||||
fi
|
||||
|
||||
php bin/console lexik:jwt:generate-keypair || true
|
||||
php bin/console app:update-rdap-servers
|
||||
|
||||
setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var
|
||||
setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var
|
||||
fi
|
||||
|
||||
exec docker-php-entrypoint "$@"
|
||||
4
frankenphp/worker.Caddyfile
Normal file
4
frankenphp/worker.Caddyfile
Normal file
@@ -0,0 +1,4 @@
|
||||
worker {
|
||||
file ./public/index.php
|
||||
env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Returns the importmap for this application.
|
||||
*
|
||||
* - "path" is a path inside the asset mapper system. Use the
|
||||
* "debug:asset-map" command to see the full list of paths.
|
||||
*
|
||||
* - "entrypoint" (JavaScript only) set to true for any module that will
|
||||
* be used as an "entrypoint" (and passed to the importmap() Twig function).
|
||||
*
|
||||
* The "importmap:require" command can be used to add new entries to this file.
|
||||
*/
|
||||
return [
|
||||
'app' => [
|
||||
'path' => './assets/app.js',
|
||||
'entrypoint' => true,
|
||||
],
|
||||
'@hotwired/stimulus' => [
|
||||
'version' => '3.2.2',
|
||||
],
|
||||
'@symfony/stimulus-bundle' => [
|
||||
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
|
||||
],
|
||||
'@hotwired/turbo' => [
|
||||
'version' => '7.3.0',
|
||||
],
|
||||
];
|
||||
32
migrations/Version20240816185909.php
Normal file
32
migrations/Version20240816185909.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20240816185909 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE watch_list ADD webhook_dsn TEXT DEFAULT NULL');
|
||||
$this->addSql('COMMENT ON COLUMN watch_list.webhook_dsn IS \'(DC2Type:simple_array)\'');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE watch_list DROP webhook_dsn');
|
||||
}
|
||||
}
|
||||
33
migrations/Version20240901190812.php
Normal file
33
migrations/Version20240901190812.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20240901190812 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE domain_event ADD deleted BOOLEAN NOT NULL DEFAULT FALSE');
|
||||
$this->addSql('ALTER TABLE entity_event ADD deleted BOOLEAN NOT NULL DEFAULT FALSE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE domain_event DROP deleted');
|
||||
$this->addSql('ALTER TABLE entity_event DROP deleted');
|
||||
}
|
||||
}
|
||||
34
migrations/Version20240904155605.php
Normal file
34
migrations/Version20240904155605.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20240904155605 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE domain_entity ADD deleted BOOLEAN NOT NULL DEFAULT FALSE');
|
||||
$this->addSql('ALTER TABLE domain_entity DROP updated_at');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE domain_entity ADD updated_at DATE NOT NULL');
|
||||
$this->addSql('ALTER TABLE domain_entity DROP deleted');
|
||||
$this->addSql('COMMENT ON COLUMN domain_entity.updated_at IS \'(DC2Type:date_immutable)\'');
|
||||
}
|
||||
}
|
||||
33
migrations/Version20240904162520.php
Normal file
33
migrations/Version20240904162520.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20240904162520 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE domain ALTER updated_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
|
||||
$this->addSql('COMMENT ON COLUMN domain.updated_at IS \'(DC2Type:datetime_immutable)\'');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE domain ALTER updated_at TYPE DATE');
|
||||
$this->addSql('COMMENT ON COLUMN domain.updated_at IS \'(DC2Type:date_immutable)\'');
|
||||
}
|
||||
}
|
||||
11
package.json
11
package.json
@@ -19,18 +19,26 @@
|
||||
"@babel/preset-env": "^7.16.0",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@fontsource/noto-color-emoji": "^5.0.27",
|
||||
"@hotwired/stimulus": "^3.0.0",
|
||||
"@hotwired/turbo": "^7.1.1 || ^8.0",
|
||||
"@symfony/stimulus-bridge": "^3.2.0",
|
||||
"@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/assets",
|
||||
"@symfony/webpack-encore": "^4.0.0",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/dagre": "^0.7.52",
|
||||
"@types/jsonld": "^1.5.15",
|
||||
"@types/punycode": "^2.1.4",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-responsive": "^8.0.8",
|
||||
"@types/vcf": "^2.0.7",
|
||||
"@xyflow/react": "^12.1.0",
|
||||
"antd": "^5.19.3",
|
||||
"axios": "^1.7.2",
|
||||
"core-js": "^3.23.0",
|
||||
"dagre": "^0.8.5",
|
||||
"html-loader": "^5.1.0",
|
||||
"html-to-image": "^1.11.11",
|
||||
"jsonld": "^8.3.2",
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^18.3.1",
|
||||
@@ -58,8 +66,5 @@
|
||||
"ttag:po2json": "cd translations; for i in $(find . -name \"*.po\"); do ttag po2json $i > ../public/locales/$i.json; done; cd ..",
|
||||
"ttag:extract": "ttag extract $(find assets -name '*.ts' -or -name '*.tsx') -o translations/translations.pot"
|
||||
},
|
||||
"dependencies": {
|
||||
"remove": "^0.1.5"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
],
|
||||
"scope": "/",
|
||||
"start_url": "/",
|
||||
"orientation": "landscape",
|
||||
"display": "standalone",
|
||||
"background_color": "#fff",
|
||||
"description": "A standalone app that collects open access information about domain names, helping users track the history and changes associated with domain names. "
|
||||
|
||||
@@ -2,22 +2,19 @@
|
||||
|
||||
namespace App\Config;
|
||||
|
||||
use App\Service\Connector\OvhConnector;
|
||||
use App\Service\Connector\GandiConnector;
|
||||
use App\Service\Connector\NamecheapConnector;
|
||||
use App\Config\Provider\GandiProvider;
|
||||
use App\Config\Provider\OvhProvider;
|
||||
|
||||
enum ConnectorProvider: string
|
||||
{
|
||||
case OVH = 'ovh';
|
||||
case GANDI = 'gandi';
|
||||
case NAMECHEAP = 'namecheap';
|
||||
|
||||
public function getConnectorProvider(): string
|
||||
{
|
||||
return match ($this) {
|
||||
ConnectorProvider::OVH => OvhConnector::class,
|
||||
ConnectorProvider::GANDI => GandiConnector::class,
|
||||
ConnectorProvider::NAMECHEAP => NamecheapConnector::class
|
||||
ConnectorProvider::OVH => OvhProvider::class,
|
||||
ConnectorProvider::GANDI => GandiProvider::class
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ namespace App\Config;
|
||||
enum TriggerAction: string
|
||||
{
|
||||
case SendEmail = 'email';
|
||||
case SendChat = 'chat';
|
||||
}
|
||||
|
||||
47
src/Config/WebhookScheme.php
Normal file
47
src/Config/WebhookScheme.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Config;
|
||||
|
||||
use Symfony\Component\Notifier\Bridge\Discord\DiscordTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Engagespot\EngagespotTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Ntfy\NtfyTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Pushover\PushoverTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\RocketChat\RocketChatTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Slack\SlackTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Zulip\ZulipTransportFactory;
|
||||
|
||||
enum WebhookScheme: string
|
||||
{
|
||||
case DISCORD = 'discord';
|
||||
case GOOGLE_CHAT = 'googlechat';
|
||||
case MATTERMOST = 'mattermost';
|
||||
case MICROSOFT_TEAMS = 'microsoftteams';
|
||||
case ROCKET_CHAT = 'rocketchat';
|
||||
case SLACK = 'slack';
|
||||
case TELEGRAM = 'telegram';
|
||||
case ZULIP = 'zulip';
|
||||
case PUSHOVER = 'pushover';
|
||||
case NTFY = 'ntfy';
|
||||
case ENGAGESPOT = 'engagespot';
|
||||
|
||||
public function getChatTransportFactory(): string
|
||||
{
|
||||
return match ($this) {
|
||||
WebhookScheme::DISCORD => DiscordTransportFactory::class,
|
||||
WebhookScheme::GOOGLE_CHAT => GoogleChatTransportFactory::class,
|
||||
WebhookScheme::MATTERMOST => MattermostTransportFactory::class,
|
||||
WebhookScheme::MICROSOFT_TEAMS => MicrosoftTeamsTransportFactory::class,
|
||||
WebhookScheme::ROCKET_CHAT => RocketChatTransportFactory::class,
|
||||
WebhookScheme::SLACK => SlackTransportFactory::class,
|
||||
WebhookScheme::TELEGRAM => TelegramTransportFactory::class,
|
||||
WebhookScheme::ZULIP => ZulipTransportFactory::class,
|
||||
WebhookScheme::PUSHOVER => PushoverTransportFactory::class,
|
||||
WebhookScheme::NTFY => NtfyTransportFactory::class,
|
||||
WebhookScheme::ENGAGESPOT => EngagespotTransportFactory::class
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Config\Connector\ConnectorInterface;
|
||||
use App\Entity\Connector;
|
||||
use App\Entity\User;
|
||||
use App\Service\Connector\AbstractProvider;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class ConnectorController extends AbstractController
|
||||
@@ -43,7 +43,6 @@ class ConnectorController extends AbstractController
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
#[Route(
|
||||
path: '/api/connectors',
|
||||
@@ -69,10 +68,10 @@ class ConnectorController extends AbstractController
|
||||
]);
|
||||
|
||||
if (null === $provider) {
|
||||
throw new \Exception('Provider not found');
|
||||
throw new BadRequestHttpException('Provider not found');
|
||||
}
|
||||
|
||||
/** @var ConnectorInterface $connectorProviderClass */
|
||||
/** @var AbstractProvider $connectorProviderClass */
|
||||
$connectorProviderClass = $provider->getConnectorProvider();
|
||||
|
||||
$authData = $connectorProviderClass::verifyAuthData($connector->getAuthData(), $client);
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\WatchList;
|
||||
use App\Message\ProcessDomainTrigger;
|
||||
use App\Message\SendDomainEventNotif;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Service\RDAPService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -25,7 +25,7 @@ class DomainRefreshController extends AbstractController
|
||||
private readonly RDAPService $RDAPService,
|
||||
private readonly RateLimiterFactory $rdapRequestsLimiter,
|
||||
private readonly MessageBusInterface $bus,
|
||||
private readonly LoggerInterface $logger
|
||||
private readonly LoggerInterface $logger, private readonly KernelInterface $kernel
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -35,8 +35,9 @@ class DomainRefreshController extends AbstractController
|
||||
* @throws ExceptionInterface
|
||||
* @throws \Exception
|
||||
* @throws HttpExceptionInterface
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function __invoke(string $ldhName, KernelInterface $kernel): ?Domain
|
||||
public function __invoke(string $ldhName, KernelInterface $kernel): Domain
|
||||
{
|
||||
$idnDomain = strtolower(idn_to_ascii($ldhName));
|
||||
$userId = $this->getUser()->getUserIdentifier();
|
||||
@@ -53,7 +54,8 @@ class DomainRefreshController extends AbstractController
|
||||
if (null !== $domain
|
||||
&& !$domain->getDeleted()
|
||||
&& ($domain->getUpdatedAt()->diff(new \DateTimeImmutable('now'))->days < 7)
|
||||
&& !$this->RDAPService::isToBeWatchClosely($domain, $domain->getUpdatedAt())
|
||||
&& !$this->RDAPService::isToBeWatchClosely($domain)
|
||||
&& !$this->kernel->isDebug()
|
||||
) {
|
||||
$this->logger->info('It is not necessary to update the information of the domain name {idnDomain} with the RDAP protocol.', [
|
||||
'idnDomain' => $idnDomain,
|
||||
@@ -79,7 +81,7 @@ class DomainRefreshController extends AbstractController
|
||||
|
||||
/** @var WatchList $watchList */
|
||||
foreach ($watchLists as $watchList) {
|
||||
$this->bus->dispatch(new ProcessDomainTrigger($watchList->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
$this->bus->dispatch(new SendDomainEventNotif($watchList->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
}
|
||||
|
||||
return $domain;
|
||||
|
||||
@@ -75,6 +75,25 @@ class RegistrationController extends AbstractController
|
||||
)
|
||||
);
|
||||
|
||||
if (false === (bool) $this->getParameter('registration_verify_email')) {
|
||||
$user->setVerified(true);
|
||||
} else {
|
||||
$email = $this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
|
||||
(new TemplatedEmail())
|
||||
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||
->to($user->getEmail())
|
||||
->locale('en')
|
||||
->subject('Please Confirm your Email')
|
||||
->htmlTemplate('emails/success/confirmation_email.html.twig')
|
||||
);
|
||||
|
||||
$signedUrl = (string) $email->getContext()['signedUrl'];
|
||||
$this->logger->notice('The validation link for user {username} is {signedUrl}', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'signedUrl' => $signedUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
|
||||
@@ -82,15 +101,6 @@ class RegistrationController extends AbstractController
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
$this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
|
||||
(new TemplatedEmail())
|
||||
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||
->to($user->getEmail())
|
||||
->locale('en')
|
||||
->subject('Please Confirm your Email')
|
||||
->htmlTemplate('emails/success/confirmation_email.html.twig')
|
||||
);
|
||||
|
||||
return new Response(null, 201);
|
||||
}
|
||||
|
||||
|
||||
80
src/Controller/StatisticsController.php
Normal file
80
src/Controller/StatisticsController.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Statistics;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchListRepository;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
|
||||
class StatisticsController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CacheItemPoolInterface $pool,
|
||||
private readonly DomainRepository $domainRepository,
|
||||
private readonly WatchListRepository $watchListRepository,
|
||||
private readonly KernelInterface $kernel
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __invoke(): Statistics
|
||||
{
|
||||
$stats = new Statistics();
|
||||
|
||||
$stats
|
||||
->setRdapQueries($this->pool->getItem('stats.rdap_queries.count')->get() ?? 0)
|
||||
->setDomainPurchased($this->pool->getItem('stats.domain.purchased')->get() ?? 0)
|
||||
->setDomainPurchaseFailed($this->pool->getItem('stats.domain.purchase.failed')->get() ?? 0)
|
||||
->setAlertSent($this->pool->getItem('stats.alert.sent')->get() ?? 0)
|
||||
|
||||
->setDomainTracked(
|
||||
$this->getCachedItem('stats.domain.tracked', fn () => $this->watchListRepository->createQueryBuilder('w')
|
||||
->join('w.domains', 'd')
|
||||
->select('COUNT(DISTINCT d.ldhName)')
|
||||
->where('d.deleted = FALSE')
|
||||
->getQuery()->getSingleColumnResult()[0])
|
||||
)
|
||||
->setDomainCount(
|
||||
$this->getCachedItem('stats.domain.count', fn () => $this->domainRepository->createQueryBuilder('d')
|
||||
->join('d.tld', 't')
|
||||
->select('t.tld tld')
|
||||
->addSelect('COUNT(d.ldhName) AS domain')
|
||||
->addGroupBy('t.tld')
|
||||
->where('d.deleted = FALSE')
|
||||
->orderBy('domain', 'DESC')
|
||||
->setMaxResults(5)
|
||||
->getQuery()->getArrayResult())
|
||||
)
|
||||
->setDomainCountTotal(
|
||||
$this->getCachedItem('stats.domain.total', fn () => $this->domainRepository->count(['deleted' => false])
|
||||
));
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function getCachedItem(string $key, callable $getItemFunction)
|
||||
{
|
||||
$item = $this->pool->getItem($key);
|
||||
|
||||
if (!$item->isHit() || $this->kernel->isDebug()) {
|
||||
$value = $getItemFunction();
|
||||
$item
|
||||
->set($value)
|
||||
->expiresAfter(new \DateInterval('PT6H'));
|
||||
$this->pool->save($item);
|
||||
|
||||
return $value;
|
||||
} else {
|
||||
return $item->get();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Connector;
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\DomainEntity;
|
||||
use App\Entity\DomainEvent;
|
||||
use App\Entity\User;
|
||||
use App\Entity\WatchList;
|
||||
use App\Notifier\TestChatNotification;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Service\ChatNotificationService;
|
||||
use App\Service\Connector\AbstractProvider;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Eluceo\iCal\Domain\Entity\Attendee;
|
||||
use Eluceo\iCal\Domain\Entity\Calendar;
|
||||
use Eluceo\iCal\Domain\Entity\Event;
|
||||
@@ -22,6 +27,7 @@ use Eluceo\iCal\Domain\ValueObject\Timestamp;
|
||||
use Eluceo\iCal\Presentation\Component\Property;
|
||||
use Eluceo\iCal\Presentation\Component\Property\Value\TextValue;
|
||||
use Eluceo\iCal\Presentation\Factory\CalendarFactory;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\VObject\EofException;
|
||||
use Sabre\VObject\InvalidDataException;
|
||||
@@ -31,8 +37,11 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class WatchListController extends AbstractController
|
||||
{
|
||||
@@ -40,7 +49,11 @@ class WatchListController extends AbstractController
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly WatchListRepository $watchListRepository,
|
||||
private readonly LoggerInterface $logger
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly CacheItemPoolInterface $cacheItemPool,
|
||||
private readonly KernelInterface $kernel,
|
||||
private readonly ChatNotificationService $chatNotificationService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -69,8 +82,8 @@ class WatchListController extends AbstractController
|
||||
* This policy guarantees the equal probability of obtaining a domain name if it is requested by several users.
|
||||
*/
|
||||
if ($this->getParameter('limited_features')) {
|
||||
if ($watchList->getDomains()->count() >= (int) $this->getParameter('limit_max_watchlist_domains')) {
|
||||
$this->logger->notice('User {username} tried to create a Watchlist. However, the maximum number of domains has been reached for this Watchlist', [
|
||||
if ($watchList->getDomains()->count() > (int) $this->getParameter('limit_max_watchlist_domains')) {
|
||||
$this->logger->notice('User {username} tried to create a Watchlist. The maximum number of domains has been reached.', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
throw new AccessDeniedHttpException('You have exceeded the maximum number of domain names allowed in this Watchlist');
|
||||
@@ -78,7 +91,7 @@ class WatchListController extends AbstractController
|
||||
|
||||
$userWatchLists = $user->getWatchLists();
|
||||
if ($userWatchLists->count() >= (int) $this->getParameter('limit_max_watchlist')) {
|
||||
$this->logger->notice('User {username} tried to create a Watchlist. However, the maximum number of Watchlists has been reached.', [
|
||||
$this->logger->notice('User {username} tried to create a Watchlist. The maximum number of Watchlists has been reached', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
throw new AccessDeniedHttpException('You have exceeded the maximum number of Watchlists allowed');
|
||||
@@ -90,17 +103,29 @@ class WatchListController extends AbstractController
|
||||
/** @var Domain $domain */
|
||||
foreach ($watchList->getDomains()->getIterator() as $domain) {
|
||||
if (in_array($domain, $trackedDomains)) {
|
||||
$this->logger->notice('User {username} tried to create a watchlist with domain name {ldhName}. However, it is forbidden to register the same domain name twice with limited mode.', [
|
||||
$ldhName = $domain->getLdhName();
|
||||
|
||||
$this->logger->notice('User {username} tried to create a watchlist with domain name {ldhName}. It is forbidden to register the same domain name twice with limited mode', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'ldhName' => $domain->getLdhName(),
|
||||
'ldhName' => $ldhName,
|
||||
]);
|
||||
|
||||
throw new AccessDeniedHttpException('It is forbidden to register the same domain name twice in your watchlists with limited mode.');
|
||||
throw new AccessDeniedHttpException("It is forbidden to register the same domain name twice in your watchlists with limited mode ($ldhName)");
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $watchList->getWebhookDsn() && count($watchList->getWebhookDsn()) > (int) $this->getParameter('limit_max_watchlist_webhooks')) {
|
||||
$this->logger->notice('User {username} tried to create a Watchlist. The maximum number of webhooks has been reached.', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
throw new AccessDeniedHttpException('You have exceeded the maximum number of webhooks allowed in this Watchlist');
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->info('User {username} register a Watchlist ({token}).', [
|
||||
$this->chatNotificationService->sendChatNotification($watchList, new TestChatNotification());
|
||||
$this->verifyConnector($watchList, $watchList->getConnector());
|
||||
|
||||
$this->logger->info('User {username} registers a Watchlist ({token}).', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'token' => $watchList->getToken(),
|
||||
]);
|
||||
@@ -111,6 +136,138 @@ class WatchListController extends AbstractController
|
||||
return $watchList;
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/api/watchlists',
|
||||
name: 'watchlist_get_all_mine',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_operation_name' => 'get_all_mine',
|
||||
],
|
||||
methods: ['GET']
|
||||
)]
|
||||
public function getWatchLists(): Collection
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
return $user->getWatchLists();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function verifyConnector(WatchList $watchList, ?Connector $connector): void
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
if (null === $connector) {
|
||||
return;
|
||||
}
|
||||
if (!$user->getConnectors()->contains($connector)) {
|
||||
$this->logger->notice('The Connector ({connector}) does not belong to the user.', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'connector' => $connector->getId(),
|
||||
]);
|
||||
throw new AccessDeniedHttpException('You cannot create a Watchlist with a connector that does not belong to you');
|
||||
}
|
||||
|
||||
/** @var Domain $domain */
|
||||
foreach ($watchList->getDomains()->getIterator() as $domain) {
|
||||
if ($domain->getDeleted()) {
|
||||
$ldhName = $domain->getLdhName();
|
||||
throw new BadRequestHttpException("To add a connector, no domain in this Watchlist must have already expired ($ldhName)");
|
||||
}
|
||||
}
|
||||
|
||||
$connectorProviderClass = $connector->getProvider()->getConnectorProvider();
|
||||
/** @var AbstractProvider $connectorProvider */
|
||||
$connectorProvider = new $connectorProviderClass($connector->getAuthData(), $this->httpClient, $this->cacheItemPool, $this->kernel);
|
||||
|
||||
$connectorProvider::verifyAuthData($connector->getAuthData(), $this->httpClient); // We want to check if the tokens are OK
|
||||
$supported = $connectorProvider->isSupported(...$watchList->getDomains()->toArray());
|
||||
|
||||
if (!$supported) {
|
||||
$this->logger->notice('The Connector ({connector}) does not support all TLDs in this Watchlist', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'connector' => $connector->getId(),
|
||||
]);
|
||||
throw new BadRequestHttpException('This connector does not support all TLDs in this Watchlist');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
* @throws \Exception
|
||||
*/
|
||||
#[Route(
|
||||
path: '/api/watchlists/{token}',
|
||||
name: 'watchlist_update',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_operation_name' => 'update',
|
||||
],
|
||||
methods: ['PUT']
|
||||
)]
|
||||
public function putWatchList(WatchList $watchList): WatchList
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$watchList->setUser($user);
|
||||
|
||||
if ($this->getParameter('limited_features')) {
|
||||
if ($watchList->getDomains()->count() > (int) $this->getParameter('limit_max_watchlist_domains')) {
|
||||
$this->logger->notice('User {username} tried to update a Watchlist. The maximum number of domains has been reached for this Watchlist', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
throw new AccessDeniedHttpException('You have exceeded the maximum number of domain names allowed in this Watchlist');
|
||||
}
|
||||
|
||||
$userWatchLists = $user->getWatchLists();
|
||||
|
||||
/** @var Domain[] $trackedDomains */
|
||||
$trackedDomains = $userWatchLists
|
||||
->filter(fn (WatchList $wl) => $wl->getToken() !== $watchList->getToken())
|
||||
->reduce(fn (array $acc, WatchList $wl) => [...$acc, ...$wl->getDomains()->toArray()], []);
|
||||
|
||||
/** @var Domain $domain */
|
||||
foreach ($watchList->getDomains()->getIterator() as $domain) {
|
||||
if (in_array($domain, $trackedDomains)) {
|
||||
$ldhName = $domain->getLdhName();
|
||||
$this->logger->notice('User {username} tried to update a watchlist with domain name {ldhName}. It is forbidden to register the same domain name twice with limited mode', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'ldhName' => $ldhName,
|
||||
]);
|
||||
|
||||
throw new AccessDeniedHttpException("It is forbidden to register the same domain name twice in your watchlists with limited mode ($ldhName)");
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $watchList->getWebhookDsn() && count($watchList->getWebhookDsn()) > (int) $this->getParameter('limit_max_watchlist_webhooks')) {
|
||||
$this->logger->notice('User {username} tried to update a Watchlist. The maximum number of webhooks has been reached.', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
throw new AccessDeniedHttpException('You have exceeded the maximum number of webhooks allowed in this Watchlist');
|
||||
}
|
||||
}
|
||||
|
||||
$this->chatNotificationService->sendChatNotification($watchList, new TestChatNotification());
|
||||
$this->verifyConnector($watchList, $watchList->getConnector());
|
||||
|
||||
$this->logger->info('User {username} updates a Watchlist ({token}).', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'token' => $watchList->getToken(),
|
||||
]);
|
||||
|
||||
$this->em->remove($this->em->getReference(WatchList::class, $watchList->getToken()));
|
||||
$this->em->flush();
|
||||
|
||||
$this->em->persist($watchList);
|
||||
$this->em->flush();
|
||||
|
||||
return $watchList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ParseException
|
||||
* @throws EofException
|
||||
@@ -149,7 +306,7 @@ class WatchListController extends AbstractController
|
||||
}
|
||||
|
||||
/** @var DomainEvent $event */
|
||||
foreach ($domain->getEvents()->toArray() as $event) {
|
||||
foreach ($domain->getEvents()->filter(fn (DomainEvent $e) => $e->getDate()->diff(new \DateTimeImmutable('now'))->y <= 10)->getIterator() as $event) {
|
||||
$calendar->addEvent((new Event())
|
||||
->setLastModified(new Timestamp($domain->getUpdatedAt()))
|
||||
->setStatus(EventStatus::CONFIRMED())
|
||||
@@ -172,20 +329,55 @@ class WatchListController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
#[Route(
|
||||
path: '/api/watchlists',
|
||||
name: 'watchlist_get_all_mine',
|
||||
path: '/api/tracked',
|
||||
name: 'watchlist_get_tracked_domains',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_operation_name' => 'get_all_mine',
|
||||
],
|
||||
methods: ['GET']
|
||||
'_api_operation_name' => 'get_tracked_domains',
|
||||
]
|
||||
)]
|
||||
public function getWatchLists(): Collection
|
||||
public function getTrackedDomains(): array
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
return $user->getWatchLists();
|
||||
$domains = [];
|
||||
/** @var WatchList $watchList */
|
||||
foreach ($user->getWatchLists()->getIterator() as $watchList) {
|
||||
/** @var Domain $domain */
|
||||
foreach ($watchList->getDomains()->getIterator() as $domain) {
|
||||
/** @var DomainEvent|null $exp */
|
||||
$exp = $domain->getEvents()->findFirst(fn (int $key, DomainEvent $e) => !$e->getDeleted() && 'expiration' === $e->getAction());
|
||||
|
||||
if (!$domain->getDeleted()
|
||||
&& null !== $exp && $exp->getDate() > new \DateTimeImmutable()
|
||||
&& count(array_filter($domain->getEvents()->toArray(), fn (DomainEvent $e) => !$e->getDeleted() && 'expiration' === $e->getAction())) > 0
|
||||
&& !in_array($domain, $domains)) {
|
||||
$domains[] = $domain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usort($domains, function (Domain $d1, Domain $d2) {
|
||||
$IMPORTANT_STATUS = ['pending delete', 'redemption period', 'auto renew period'];
|
||||
|
||||
/** @var \DateTimeImmutable $exp1 */
|
||||
$exp1 = $d1->getEvents()->findFirst(fn (int $key, DomainEvent $e) => !$e->getDeleted() && 'expiration' === $e->getAction())->getDate();
|
||||
/** @var \DateTimeImmutable $exp2 */
|
||||
$exp2 = $d2->getEvents()->findFirst(fn (int $key, DomainEvent $e) => !$e->getDeleted() && 'expiration' === $e->getAction())->getDate();
|
||||
$impStatus1 = count(array_intersect($IMPORTANT_STATUS, $d1->getStatus())) > 0;
|
||||
$impStatus2 = count(array_intersect($IMPORTANT_STATUS, $d2->getStatus())) > 0;
|
||||
|
||||
return $impStatus1 && !$impStatus2 ? -1 : (
|
||||
!$impStatus1 && $impStatus2 ? 2 :
|
||||
$exp1 <=> $exp2
|
||||
);
|
||||
});
|
||||
|
||||
return $domains;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ use Symfony\Component\Uid\Uuid;
|
||||
normalizationContext: ['groups' => ['connector:create', 'connector:list']], denormalizationContext: ['groups' => 'connector:create'],
|
||||
name: 'create'
|
||||
),
|
||||
new Delete(),
|
||||
new Delete(
|
||||
security: 'object.user == user'
|
||||
),
|
||||
]
|
||||
)]
|
||||
#[ORM\Entity(repositoryClass: ConnectorRepository::class)]
|
||||
|
||||
@@ -57,7 +57,7 @@ class Domain
|
||||
* @var Collection<int, DomainEvent>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: DomainEvent::class, mappedBy: 'domain', cascade: ['persist'], orphanRemoval: true)]
|
||||
#[Groups(['domain:item'])]
|
||||
#[Groups(['domain:item', 'domain:list'])]
|
||||
private Collection $events;
|
||||
|
||||
/**
|
||||
@@ -69,7 +69,7 @@ class Domain
|
||||
private Collection $domainEntities;
|
||||
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)]
|
||||
#[Groups(['domain:item'])]
|
||||
#[Groups(['domain:item', 'domain:list'])]
|
||||
private array $status = [];
|
||||
|
||||
/**
|
||||
@@ -90,19 +90,20 @@ class Domain
|
||||
private Collection $nameservers;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
private ?\DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
|
||||
private ?\DateTimeImmutable $updatedAt = null;
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
#[Groups(['domain:item', 'domain:list'])]
|
||||
private ?\DateTimeImmutable $updatedAt;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(referencedColumnName: 'tld', nullable: false)]
|
||||
#[Groups(['domain:item'])]
|
||||
#[Groups(['domain:item', 'domain:list'])]
|
||||
private ?Tld $tld = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['domain:item'])]
|
||||
private ?bool $deleted = null;
|
||||
#[ORM\Column(nullable: false)]
|
||||
#[Groups(['domain:item', 'domain:list'])]
|
||||
private ?bool $deleted;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
@@ -27,13 +27,13 @@ class DomainEntity
|
||||
#[Groups(['domain-entity:entity', 'domain-entity:domain'])]
|
||||
private array $roles = [];
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
|
||||
#[ORM\Column]
|
||||
#[Groups(['domain-entity:entity', 'domain-entity:domain'])]
|
||||
private ?\DateTimeImmutable $updatedAt = null;
|
||||
private ?bool $deleted;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->updatedAt = new \DateTimeImmutable('now');
|
||||
$this->deleted = false;
|
||||
}
|
||||
|
||||
public function getDomain(): ?Domain
|
||||
@@ -75,22 +75,15 @@ class DomainEntity
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): ?\DateTimeImmutable
|
||||
public function getDeleted(): ?bool
|
||||
{
|
||||
return $this->updatedAt;
|
||||
return $this->deleted;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(\DateTimeImmutable $updatedAt): static
|
||||
public function setDeleted(?bool $deleted): static
|
||||
{
|
||||
$this->updatedAt = $updatedAt;
|
||||
$this->deleted = $deleted;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
#[ORM\PreUpdate]
|
||||
public function updateTimestamps(): void
|
||||
{
|
||||
$this->setUpdatedAt(new \DateTimeImmutable('now'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
#[ORM\Entity(repositoryClass: DomainEventRepository::class)]
|
||||
class DomainEvent extends Event
|
||||
{
|
||||
#[ORM\ManyToOne(targetEntity: Domain::class, cascade: ['persist'], inversedBy: 'events')]
|
||||
#[ORM\ManyToOne(targetEntity: Domain::class, inversedBy: 'events')]
|
||||
#[ORM\JoinColumn(referencedColumnName: 'ldh_name', nullable: false)]
|
||||
private ?Domain $domain = null;
|
||||
|
||||
|
||||
@@ -21,6 +21,15 @@ class Event
|
||||
#[Groups(['event:list'])]
|
||||
private ?\DateTimeImmutable $date = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['event:list'])]
|
||||
private ?bool $deleted;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->deleted = false;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -49,4 +58,16 @@ class Event
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDeleted(): ?bool
|
||||
{
|
||||
return $this->deleted;
|
||||
}
|
||||
|
||||
public function setDeleted(?bool $deleted): static
|
||||
{
|
||||
$this->deleted = $deleted;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
111
src/Entity/Statistics.php
Normal file
111
src/Entity/Statistics.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use App\Controller\StatisticsController;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(
|
||||
uriTemplate: '/stats',
|
||||
controller: StatisticsController::class,
|
||||
shortName: 'Statistics',
|
||||
read: false,
|
||||
),
|
||||
]
|
||||
)]
|
||||
class Statistics
|
||||
{
|
||||
private ?int $rdapQueries = null;
|
||||
private ?int $alertSent = null;
|
||||
private ?int $domainPurchased = null;
|
||||
private ?int $domainPurchaseFailed = null;
|
||||
|
||||
private ?array $domainCount = null;
|
||||
private ?int $domainTracked = null;
|
||||
private ?int $domainCountTotal = null;
|
||||
|
||||
public function getRdapQueries(): ?int
|
||||
{
|
||||
return $this->rdapQueries;
|
||||
}
|
||||
|
||||
public function setRdapQueries(?int $rdapQueries): static
|
||||
{
|
||||
$this->rdapQueries = $rdapQueries;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAlertSent(): ?int
|
||||
{
|
||||
return $this->alertSent;
|
||||
}
|
||||
|
||||
public function setAlertSent(?int $alertSent): static
|
||||
{
|
||||
$this->alertSent = $alertSent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDomainPurchased(): ?int
|
||||
{
|
||||
return $this->domainPurchased;
|
||||
}
|
||||
|
||||
public function setDomainPurchased(?int $domainPurchased): static
|
||||
{
|
||||
$this->domainPurchased = $domainPurchased;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDomainCount(): ?array
|
||||
{
|
||||
return $this->domainCount;
|
||||
}
|
||||
|
||||
public function setDomainCount(?array $domainCount): static
|
||||
{
|
||||
$this->domainCount = $domainCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDomainCountTotal(): ?int
|
||||
{
|
||||
return $this->domainCountTotal;
|
||||
}
|
||||
|
||||
public function setDomainCountTotal(?int $domainCountTotal): void
|
||||
{
|
||||
$this->domainCountTotal = $domainCountTotal;
|
||||
}
|
||||
|
||||
public function getDomainPurchaseFailed(): ?int
|
||||
{
|
||||
return $this->domainPurchaseFailed;
|
||||
}
|
||||
|
||||
public function setDomainPurchaseFailed(?int $domainPurchaseFailed): static
|
||||
{
|
||||
$this->domainPurchaseFailed = $domainPurchaseFailed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDomainTracked(): ?int
|
||||
{
|
||||
return $this->domainTracked;
|
||||
}
|
||||
|
||||
public function setDomainTracked(?int $domainTracked): static
|
||||
{
|
||||
$this->domainTracked = $domainTracked;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,12 @@ use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Controller\WatchListController;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\WatchListRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
@@ -26,13 +26,31 @@ use Symfony\Component\Uid\Uuid;
|
||||
normalizationContext: ['groups' => 'watchlist:list'],
|
||||
name: 'get_all_mine',
|
||||
),
|
||||
new GetCollection(
|
||||
uriTemplate: '/tracked',
|
||||
routeName: 'watchlist_get_tracked_domains',
|
||||
normalizationContext: ['groups' => [
|
||||
'domain:list',
|
||||
'tld:list',
|
||||
'event:list',
|
||||
]],
|
||||
name: 'get_tracked_domains'
|
||||
),
|
||||
new Get(
|
||||
normalizationContext: ['groups' => 'watchlist:item'],
|
||||
normalizationContext: ['groups' => [
|
||||
'watchlist:item',
|
||||
'domain:item',
|
||||
'event:list',
|
||||
'domain-entity:entity',
|
||||
'nameserver-entity:nameserver',
|
||||
'nameserver-entity:entity',
|
||||
'tld:item',
|
||||
],
|
||||
],
|
||||
security: 'object.user == user'
|
||||
),
|
||||
new Get(
|
||||
routeName: 'watchlist_calendar',
|
||||
controller: WatchListController::class,
|
||||
openapiContext: [
|
||||
'responses' => [
|
||||
'200' => [
|
||||
@@ -58,24 +76,27 @@ use Symfony\Component\Uid\Uuid;
|
||||
denormalizationContext: ['groups' => 'watchlist:create'],
|
||||
name: 'create'
|
||||
),
|
||||
new Patch(
|
||||
new Put(
|
||||
routeName: 'watchlist_update',
|
||||
normalizationContext: ['groups' => 'watchlist:item'],
|
||||
denormalizationContext: ['groups' => 'watchlist:update']
|
||||
denormalizationContext: ['groups' => ['watchlist:create', 'watchlist:token']],
|
||||
security: 'object.user == user',
|
||||
name: 'update'
|
||||
),
|
||||
new Delete(
|
||||
security: 'object.user == user'
|
||||
),
|
||||
new Delete(),
|
||||
],
|
||||
)]
|
||||
class WatchList
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid')]
|
||||
#[Groups(['watchlist:item', 'watchlist:list'])]
|
||||
private string $token;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'watchLists')]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
public ?User $user = null;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid')]
|
||||
#[Groups(['watchlist:item', 'watchlist:list', 'watchlist:token'])]
|
||||
private string $token;
|
||||
/**
|
||||
* @var Collection<int, Domain>
|
||||
*/
|
||||
@@ -83,29 +104,34 @@ class WatchList
|
||||
#[ORM\JoinTable(name: 'watch_lists_domains',
|
||||
joinColumns: [new ORM\JoinColumn(name: 'watch_list_token', referencedColumnName: 'token', onDelete: 'CASCADE')],
|
||||
inverseJoinColumns: [new ORM\JoinColumn(name: 'domain_ldh_name', referencedColumnName: 'ldh_name', onDelete: 'CASCADE')])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private Collection $domains;
|
||||
|
||||
/**
|
||||
* @var Collection<int, WatchListTrigger>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: WatchListTrigger::class, mappedBy: 'watchList', cascade: ['persist'], orphanRemoval: true)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
#[SerializedName('triggers')]
|
||||
private Collection $watchListTriggers;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'watchLists')]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private ?Connector $connector = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['watchlist:list', 'watchlist:item'])]
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
|
||||
#[SerializedName('dsn')]
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private ?array $webhookDsn = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->token = Uuid::v4();
|
||||
@@ -119,6 +145,11 @@ class WatchList
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function setToken(string $token): void
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
@@ -220,4 +251,16 @@ class WatchList
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWebhookDsn(): ?array
|
||||
{
|
||||
return $this->webhookDsn;
|
||||
}
|
||||
|
||||
public function setWebhookDsn(?array $webhookDsn): static
|
||||
{
|
||||
$this->webhookDsn = $webhookDsn;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,17 +12,17 @@ class WatchListTrigger
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private ?string $event = null;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\ManyToOne(targetEntity: WatchList::class, inversedBy: 'watchListTriggers')]
|
||||
#[ORM\ManyToOne(targetEntity: WatchList::class, cascade: ['persist'], inversedBy: 'watchListTriggers')]
|
||||
#[ORM\JoinColumn(referencedColumnName: 'token', nullable: false, onDelete: 'CASCADE')]
|
||||
private ?WatchList $watchList = null;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(enumType: TriggerAction::class)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private ?TriggerAction $action = null;
|
||||
|
||||
public function getEvent(): ?string
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Message;
|
||||
|
||||
final class ProcessDomainTrigger
|
||||
final class OrderDomain
|
||||
{
|
||||
public function __construct(
|
||||
public string $watchListToken,
|
||||
13
src/Message/SendDomainEventNotif.php
Normal file
13
src/Message/SendDomainEventNotif.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Message;
|
||||
|
||||
final class SendDomainEventNotif
|
||||
{
|
||||
public function __construct(
|
||||
public string $watchListToken,
|
||||
public string $ldhName,
|
||||
public \DateTimeImmutable $updatedAt
|
||||
) {
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user