mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-18 02:05:36 +00:00
chore: merge develop
This commit is contained in:
commit
aff37f7a81
21
.env.test
21
.env.test
@ -2,3 +2,24 @@
|
||||
KERNEL_CLASS='App\Kernel'
|
||||
APP_SECRET='$ecretf0rt3st'
|
||||
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||
DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:5432/postgres?serverVersion=16&charset=utf8"
|
||||
|
||||
# FEATURES
|
||||
LIMITED_FEATURES=true
|
||||
LIMIT_MAX_WATCHLIST=10
|
||||
LIMIT_MAX_WATCHLIST_DOMAINS=10
|
||||
LIMIT_MAX_WATCHLIST_WEBHOOKS=10
|
||||
|
||||
# TEST
|
||||
GANDI_PAT_TOKEN=
|
||||
|
||||
NAMECOM_USERNAME=
|
||||
NAMECOM_PASSWORD=
|
||||
|
||||
NAMECHEAP_USERNAME=
|
||||
NAMECHEAP_TOKEN=
|
||||
|
||||
# Typically your IP address, this envvar is required for
|
||||
# some connectors that need to be provided with your host's
|
||||
# outgoing IP address.
|
||||
OUTGOING_IP=
|
||||
|
||||
191
.github/workflows/lint-and-tests.yml
vendored
Normal file
191
.github/workflows/lint-and-tests.yml
vendored
Normal file
@ -0,0 +1,191 @@
|
||||
name: Lint and Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master", "develop" ]
|
||||
pull_request:
|
||||
branches: [ "master", "develop" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
php-setup:
|
||||
name: PHP Setup
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
vendor-path: ${{ steps.upload.outputs.artifact-path }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.4'
|
||||
extensions: mbstring, xml, intl, curl, iconv, pdo_pgsql, sodium, zip, http
|
||||
|
||||
- name: Cache Composer dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: vendor
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-composer-
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --prefer-dist --no-progress --no-suggest --optimize-autoloader
|
||||
|
||||
- name: Upload vendor folder
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: php-vendor
|
||||
path: vendor
|
||||
id: upload
|
||||
|
||||
|
||||
phpstan:
|
||||
name: PHPStan
|
||||
runs-on: ubuntu-latest
|
||||
needs: php-setup
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download PHP vendor
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: php-vendor
|
||||
path: vendor
|
||||
|
||||
- name: Set executable permissions
|
||||
run: chmod +x vendor/bin/*
|
||||
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.4'
|
||||
|
||||
- name: Run PHPStan
|
||||
run: vendor/bin/phpstan analyse
|
||||
|
||||
|
||||
cs-fixer:
|
||||
name: PHP-CS-Fixer
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ php-setup, phpstan ]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download PHP vendor
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: php-vendor
|
||||
path: vendor
|
||||
|
||||
- name: Set executable permissions
|
||||
run: chmod +x vendor/bin/*
|
||||
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.4'
|
||||
|
||||
- name: Run PHP-CS-Fixer
|
||||
run: vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||
|
||||
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ php-setup, cs-fixer, phpstan ]
|
||||
env:
|
||||
GANDI_PAT_TOKEN: ${{ secrets.GANDI_PAT_TOKEN }}
|
||||
NAMECOM_USERNAME: ${{ secrets.NAMECOM_USERNAME }}
|
||||
NAMECOM_PASSWORD: ${{ secrets.NAMECOM_PASSWORD }}
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download PHP vendor
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: php-vendor
|
||||
path: vendor
|
||||
|
||||
- name: Set executable permissions
|
||||
run: chmod +x vendor/bin/*
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.4'
|
||||
extensions: mbstring, xml, intl, curl, iconv, pdo_pgsql, sodium, zip, http
|
||||
|
||||
- name: Prepare database
|
||||
run: bin/console --env=test doctrine:database:create && bin/console --env=test doctrine:migrations:migrate
|
||||
|
||||
- name: Create JWT keys
|
||||
run: bin/console lexik:jwt:generate-keypair
|
||||
|
||||
- name: Add extra RDAP servers
|
||||
run: ln -s custom_rdap_servers.example.yaml config/app/custom_rdap_servers.yaml
|
||||
|
||||
- name: Run PHPUnit
|
||||
run: vendor/bin/phpunit --coverage-text --log-junit test-results.xml
|
||||
|
||||
- name: Publish Test Results
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
if: (!cancelled())
|
||||
with:
|
||||
files: |
|
||||
test-results.xml
|
||||
|
||||
eslint:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
needs: php-setup
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download PHP vendor
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: php-vendor
|
||||
path: vendor
|
||||
|
||||
- name: Set executable permissions
|
||||
run: chmod +x vendor/bin/*
|
||||
|
||||
|
||||
- name: Cache Node modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm install --global yarn && yarn install
|
||||
|
||||
- name: Run ESLint
|
||||
run: yarn run eslint
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@ -70,6 +70,8 @@ jobs:
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ github.repository }},name-canonical=true,push=true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
|
||||
58
.github/workflows/symfony.yml
vendored
58
.github/workflows/symfony.yml
vendored
@ -1,58 +0,0 @@
|
||||
name: Symfony CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
symfony:
|
||||
name: CI
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Cache Composer dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: vendor
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-composer-
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: mbstring, xml, intl, curl, iconv, pdo_pgsql, sodium, zip, http
|
||||
|
||||
- name: Install backend dependencies
|
||||
run: composer install --prefer-dist --no-progress --no-suggest --optimize-autoloader
|
||||
|
||||
- name: Run PHP-CS-Fixer
|
||||
run: vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||
|
||||
- name: Run PHPStan
|
||||
run: vendor/bin/phpstan analyse
|
||||
|
||||
- name: Run PHPUnit
|
||||
run: vendor/bin/phpunit --coverage-text
|
||||
|
||||
- name: Cache Node modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm install --global yarn && yarn install
|
||||
|
||||
- name: Run ESLint
|
||||
run: yarn run eslint
|
||||
@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
|
||||
# Versions
|
||||
FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream
|
||||
FROM dunglas/frankenphp:1-php8.4 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
|
||||
@ -17,9 +17,7 @@ VOLUME /app/var/
|
||||
# persistent / runtime deps
|
||||
# hadolint ignore=DL3008
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
acl \
|
||||
file \
|
||||
gettext \
|
||||
libicu-dev \
|
||||
libzip-dev \
|
||||
unzip \
|
||||
|
||||
@ -14,7 +14,6 @@ import {
|
||||
SafetyOutlined,
|
||||
SearchOutlined,
|
||||
TableOutlined,
|
||||
TeamOutlined,
|
||||
UserOutlined
|
||||
} from '@ant-design/icons'
|
||||
import {Menu} from 'antd'
|
||||
@ -46,14 +45,6 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
|
||||
disabled: !isAuthenticated,
|
||||
onClick: () => navigate('/search/domain')
|
||||
},
|
||||
{
|
||||
key: '/search/entity',
|
||||
icon: <TeamOutlined/>,
|
||||
label: t`Entity`,
|
||||
title: t`Entity Finder`,
|
||||
disabled: !isAuthenticated,
|
||||
onClick: () => navigate('/search/entity')
|
||||
},
|
||||
/*
|
||||
{
|
||||
key: 'ns-finder',
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import {Flex, List, Tag, Tooltip, Typography} from 'antd'
|
||||
import {Flex, List, Tag, Tooltip} from 'antd'
|
||||
import React from 'react'
|
||||
import type {Domain} from '../../utils/api'
|
||||
import {rdapRoleDetailTranslation, rdapRoleTranslation} from '../../utils/functions/rdapTranslation'
|
||||
import {
|
||||
icannAccreditationTranslation,
|
||||
rdapRoleDetailTranslation,
|
||||
rdapRoleTranslation
|
||||
} from '../../utils/functions/rdapTranslation'
|
||||
import {roleToAvatar} from '../../utils/functions/roleToAvatar'
|
||||
import {rolesToColor} from '../../utils/functions/rolesToColor'
|
||||
import {sortDomainEntities} from '../../utils/functions/sortDomainEntities'
|
||||
import {extractDetailsFromJCard} from '../../utils/functions/extractDetailsFromJCard'
|
||||
import {CheckCircleOutlined, CloseCircleOutlined, SettingOutlined} from "@ant-design/icons"
|
||||
|
||||
export function EntitiesList({domain}: { domain: Domain }) {
|
||||
const rdapRoleTranslated = rdapRoleTranslation()
|
||||
@ -27,11 +32,28 @@ export function EntitiesList({domain}: { domain: Domain }) {
|
||||
dataSource={sortDomainEntities(domain)}
|
||||
renderItem={(e) => {
|
||||
const details = extractDetailsFromJCard(e)
|
||||
const icannAccreditationTranslated = icannAccreditationTranslation()
|
||||
|
||||
const status = e.entity.icannAccreditation?.status as ('Terminated' | 'Accredited' | 'Reserved' | undefined)
|
||||
|
||||
return <List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={roleToAvatar(e)}
|
||||
title={<Typography.Text code>{e.entity.handle}</Typography.Text>}
|
||||
title={<Flex gap='small'>
|
||||
<Tag>{e.entity.handle}</Tag>
|
||||
{
|
||||
e.entity.icannAccreditation && status && <Tooltip
|
||||
title={e.entity.icannAccreditation.registrarName + " (" + icannAccreditationTranslated[status] + ")"}>
|
||||
<Tag icon={
|
||||
status === 'Terminated' ? <CloseCircleOutlined /> :
|
||||
status === 'Accredited' ? <CheckCircleOutlined/> : <SettingOutlined/>
|
||||
} color={
|
||||
status === 'Terminated' ? 'red' :
|
||||
status === 'Accredited' ? 'green' : 'yellow'
|
||||
}>{e.entity.icannAccreditation.id}</Tag>
|
||||
</Tooltip>
|
||||
}
|
||||
</Flex>}
|
||||
description={<>
|
||||
{details.fn && <div>👤 {details.fn}</div>}
|
||||
{details.organization && <div>🏢 {details.organization}</div>}
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
import {Button, Drawer, Form} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {WatchlistForm} from './WatchlistForm'
|
||||
import React, {useState} from 'react'
|
||||
import type {Connector} from '../../../utils/api/connectors'
|
||||
import useBreakpoint from "../../../hooks/useBreakpoint"
|
||||
|
||||
export function CreateWatchlistButton({onUpdateWatchlist, connectors}: {
|
||||
onUpdateWatchlist: (values: {
|
||||
domains: string[],
|
||||
trackedEvents: string[],
|
||||
trackedEppStatus: string[],
|
||||
token: string
|
||||
}) => Promise<void>
|
||||
connectors: Array<Connector & { id: string }>
|
||||
}) {
|
||||
const [form] = Form.useForm()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const sm = useBreakpoint('sm')
|
||||
|
||||
const showDrawer = () => setOpen(true)
|
||||
|
||||
const onClose = () => {
|
||||
setOpen(false)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type='default' block onClick={() => {
|
||||
showDrawer()
|
||||
}}>{t`Create a Watchlist`}</Button>
|
||||
<Drawer
|
||||
title={t`Create a Watchlist`}
|
||||
width={sm ? '100%' : '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
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
import {Popconfirm, theme, Typography} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import type { Watchlist} from '../../../utils/api'
|
||||
import {patchWatchlist} from '../../../utils/api'
|
||||
import {PauseCircleOutlined, PlayCircleOutlined} from '@ant-design/icons'
|
||||
import React from 'react'
|
||||
|
||||
export function DisableWatchlistButton({watchlist, onChange, enabled}: {
|
||||
watchlist: Watchlist,
|
||||
onChange: () => void,
|
||||
enabled: boolean
|
||||
}) {
|
||||
const {token} = theme.useToken()
|
||||
|
||||
return (
|
||||
enabled ?
|
||||
<Popconfirm
|
||||
title={t`Disable the Watchlist`}
|
||||
description={t`Are you sure to disable this Watchlist?`}
|
||||
onConfirm={async () => await patchWatchlist(watchlist.token, {enabled: !enabled}).then(onChange)}
|
||||
okText={t`Yes`}
|
||||
cancelText={t`No`}
|
||||
okButtonProps={{danger: true}}
|
||||
>
|
||||
<Typography.Link>
|
||||
<PauseCircleOutlined style={{color: token.colorText}} title={t`Disable the Watchlist`}/>
|
||||
</Typography.Link>
|
||||
</Popconfirm> : <Typography.Link>
|
||||
<PlayCircleOutlined style={{color: token.colorWarning}} title={t`Enable the Watchlist`}
|
||||
onClick={async () => await patchWatchlist(watchlist.token, {enabled: !enabled}).then(onChange)}/>
|
||||
</Typography.Link>
|
||||
)
|
||||
}
|
||||
@ -235,7 +235,7 @@ export function TrackedDomainTable() {
|
||||
description={t`No tracked domain names were found, please create your first Watchlist`}
|
||||
>
|
||||
<Link to='/tracking/watchlist'>
|
||||
<Button type='primary'>Create Now</Button>
|
||||
<Button type='primary'>{t`Create now`}</Button>
|
||||
</Link>
|
||||
</Empty>
|
||||
: <Skeleton loading={total === undefined}>
|
||||
|
||||
@ -5,19 +5,19 @@ import React, {useState} from 'react'
|
||||
import {EditOutlined} from '@ant-design/icons'
|
||||
import type {Connector} from '../../../utils/api/connectors'
|
||||
import type {Watchlist} from '../../../utils/api'
|
||||
import useBreakpoint from "../../../hooks/useBreakpoint"
|
||||
|
||||
export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}: {
|
||||
watchlist: Watchlist
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>
|
||||
onUpdateWatchlist: (values: { domains: string[], trackedEvents: string[], trackedEppStatus: string[], token: string }) => Promise<void>
|
||||
connectors: Array<Connector & { id: string }>
|
||||
}) {
|
||||
const [form] = Form.useForm()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const sm = useBreakpoint('sm')
|
||||
|
||||
const showDrawer = () => {
|
||||
setOpen(true)
|
||||
}
|
||||
const showDrawer = () => setOpen(true)
|
||||
|
||||
const onClose = () => {
|
||||
setOpen(false)
|
||||
@ -35,7 +35,8 @@ export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}
|
||||
{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: 'trackedEvents', value: watchlist.trackedEvents},
|
||||
{name: 'trackedEppStatus', value: watchlist.trackedEppStatus},
|
||||
{name: 'dsn', value: watchlist.dsn}
|
||||
])
|
||||
}}
|
||||
@ -43,7 +44,7 @@ export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}
|
||||
</Typography.Link>
|
||||
<Drawer
|
||||
title={t`Update a Watchlist`}
|
||||
width='80%'
|
||||
width={sm ? '100%' : '80%'}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
loading={loading}
|
||||
@ -62,7 +63,7 @@ export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}
|
||||
}}
|
||||
connectors={connectors}
|
||||
isCreation={false}
|
||||
watchList={watchlist}
|
||||
watchlist={watchlist}
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
|
||||
@ -7,25 +7,44 @@ import {DeleteWatchlistButton} from './DeleteWatchlistButton'
|
||||
import React from 'react'
|
||||
import type {Connector} from '../../../utils/api/connectors'
|
||||
import {CalendarWatchlistButton} from './CalendarWatchlistButton'
|
||||
import {rdapEventDetailTranslation, rdapEventNameTranslation} from '../../../utils/functions/rdapTranslation'
|
||||
import {
|
||||
rdapDomainStatusCodeDetailTranslation,
|
||||
rdapEventDetailTranslation,
|
||||
rdapEventNameTranslation
|
||||
} from '../../../utils/functions/rdapTranslation'
|
||||
|
||||
import {actionToColor} from '../../../utils/functions/actionToColor'
|
||||
import {DomainToTag} from '../../../utils/functions/DomainToTag'
|
||||
import type {Watchlist} from '../../../utils/api'
|
||||
import {eppStatusCodeToColor} from "../../../utils/functions/eppStatusCodeToColor"
|
||||
import {DisableWatchlistButton} from "./DisableWatchlistButton"
|
||||
|
||||
export function WatchlistCard({watchlist, onUpdateWatchlist, connectors, onDelete}: {
|
||||
export function WatchlistCard({watchlist, onUpdateWatchlist, connectors, onChange}: {
|
||||
watchlist: Watchlist
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>
|
||||
onUpdateWatchlist: (values: {
|
||||
domains: string[],
|
||||
trackedEvents: string[],
|
||||
trackedEppStatus: string[],
|
||||
token: string
|
||||
}) => Promise<void>
|
||||
connectors: Array<Connector & { id: string }>
|
||||
onDelete: () => void
|
||||
onChange: () => void
|
||||
}) {
|
||||
const rdapEventNameTranslated = rdapEventNameTranslation()
|
||||
const rdapEventDetailTranslated = rdapEventDetailTranslation()
|
||||
const rdapDomainStatusCodeDetailTranslated = rdapDomainStatusCodeDetailTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
aria-disabled={true}
|
||||
type='inner'
|
||||
style={{
|
||||
width: '100%',
|
||||
opacity: watchlist.enabled ? 1 : 0.5,
|
||||
filter: watchlist.enabled ? 'none' : 'grayscale(0.7)',
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
title={<>
|
||||
{
|
||||
(watchlist.connector != null)
|
||||
@ -41,7 +60,6 @@ export function WatchlistCard({watchlist, onUpdateWatchlist, connectors, onDelet
|
||||
</Tooltip>
|
||||
</>}
|
||||
size='small'
|
||||
style={{width: '100%'}}
|
||||
extra={
|
||||
<Space size='middle'>
|
||||
<ViewDiagramWatchlistButton token={watchlist.token}/>
|
||||
@ -54,26 +72,65 @@ export function WatchlistCard({watchlist, onUpdateWatchlist, connectors, onDelet
|
||||
connectors={connectors}
|
||||
/>
|
||||
|
||||
<DeleteWatchlistButton watchlist={watchlist} onDelete={onDelete}/>
|
||||
<DisableWatchlistButton watchlist={watchlist} onChange={onChange}
|
||||
enabled={watchlist.enabled}/>
|
||||
<DeleteWatchlistButton watchlist={watchlist} onDelete={onChange}/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Card.Meta description={watchlist.token} style={{marginBottom: '1em'}}/>
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
{watchlist.domains.map(d => <DomainToTag key={d.ldhName} domain={d}/>)}
|
||||
{watchlist.domains.map(d => (
|
||||
<DomainToTag key={d.ldhName} domain={d}/>
|
||||
))}
|
||||
</Col>
|
||||
|
||||
<Col span={8}>
|
||||
{watchlist.triggers?.filter(t => t.action === 'email')
|
||||
.map(t => <Tooltip
|
||||
key={t.event}
|
||||
title={rdapEventDetailTranslated[t.event as keyof typeof rdapEventDetailTranslated] || undefined}
|
||||
>
|
||||
<Tag color={actionToColor(t.event)}>
|
||||
{rdapEventNameTranslated[t.event as keyof typeof rdapEventNameTranslated]}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
<>
|
||||
<div style={{
|
||||
fontWeight: 500,
|
||||
marginBottom: '0.5em',
|
||||
color: '#555',
|
||||
fontSize: '0.9em'
|
||||
}}>
|
||||
{t`Tracked events`}
|
||||
</div>
|
||||
<div style={{marginBottom: '1em'}}>
|
||||
{watchlist.trackedEvents?.map(t => (
|
||||
<Tooltip
|
||||
key={t}
|
||||
title={rdapEventDetailTranslated[t as keyof typeof rdapEventDetailTranslated]}
|
||||
>
|
||||
<Tag color={actionToColor(t)} style={{marginBottom: 4}}>
|
||||
{rdapEventNameTranslated[t as keyof typeof rdapEventNameTranslated]}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
<>
|
||||
<div style={{
|
||||
fontWeight: 500,
|
||||
marginBottom: '0.5em',
|
||||
color: '#555',
|
||||
fontSize: '0.9em'
|
||||
}}>
|
||||
{t`Tracked EPP status`}
|
||||
</div>
|
||||
<div>
|
||||
{watchlist.trackedEppStatus?.map(t => (
|
||||
<Tooltip
|
||||
key={t}
|
||||
title={rdapDomainStatusCodeDetailTranslated[t as keyof typeof rdapDomainStatusCodeDetailTranslated]}
|
||||
>
|
||||
<Tag color={eppStatusCodeToColor(t)} style={{marginBottom: 4}}>
|
||||
{t}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
@ -2,14 +2,18 @@ import type { FormInstance, SelectProps} from 'antd'
|
||||
import {Button, Form, Input, Select, Space, Tag, Tooltip, Typography} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {ApiOutlined, MinusCircleOutlined, PlusOutlined} from '@ant-design/icons'
|
||||
import React, {useState} from 'react'
|
||||
import React from 'react'
|
||||
import type {Connector} from '../../../utils/api/connectors'
|
||||
import {rdapEventDetailTranslation, rdapEventNameTranslation} from '../../../utils/functions/rdapTranslation'
|
||||
import {
|
||||
rdapDomainStatusCodeDetailTranslation,
|
||||
rdapEventDetailTranslation,
|
||||
rdapEventNameTranslation
|
||||
} from '../../../utils/functions/rdapTranslation'
|
||||
import {actionToColor} from '../../../utils/functions/actionToColor'
|
||||
import {actionToIcon} from '../../../utils/functions/actionToIcon'
|
||||
import type {EventAction, Watchlist} from '../../../utils/api'
|
||||
import { createWatchlistTrigger, deleteWatchlistTrigger} from '../../../utils/api'
|
||||
import {formItemLayoutWithOutLabel} from "../../../utils/providers"
|
||||
import {eppStatusCodeToColor} from "../../../utils/functions/eppStatusCodeToColor"
|
||||
|
||||
type TagRender = SelectProps['tagRender']
|
||||
|
||||
@ -24,17 +28,18 @@ const formItemLayout = {
|
||||
}
|
||||
}
|
||||
|
||||
export function WatchlistForm({form, connectors, onFinish, isCreation, watchList}: {
|
||||
export function WatchlistForm({form, connectors, onFinish, isCreation}: {
|
||||
form: FormInstance
|
||||
connectors: Array<Connector & { id: string }>
|
||||
onFinish: (values: { domains: string[], triggers: string[], token: string }) => void
|
||||
onFinish: (values: { domains: string[], trackedEvents: string[], trackedEppStatus: string[], token: string }) => void
|
||||
isCreation: boolean,
|
||||
watchList?: Watchlist,
|
||||
watchlist?: Watchlist,
|
||||
}) {
|
||||
const rdapEventNameTranslated = rdapEventNameTranslation()
|
||||
const rdapEventDetailTranslated = rdapEventDetailTranslation()
|
||||
const rdapDomainStatusCodeDetailTranslated = rdapDomainStatusCodeDetailTranslation()
|
||||
|
||||
const triggerTagRenderer: TagRender = ({value, closable, onClose}: {
|
||||
const eventActionTagRenderer: TagRender = ({value, closable, onClose}: {
|
||||
value: EventAction
|
||||
closable: boolean
|
||||
onClose: () => void
|
||||
@ -61,40 +66,30 @@ export function WatchlistForm({form, connectors, onFinish, isCreation, watchList
|
||||
)
|
||||
}
|
||||
|
||||
const [triggersLoading, setTriggersLoading] = useState(false)
|
||||
|
||||
const createTrigger = async (event: string) => {
|
||||
if (isCreation) return
|
||||
|
||||
setTriggersLoading(true)
|
||||
await createWatchlistTrigger(watchList!.token, {
|
||||
watchList: watchList!['@id'],
|
||||
event,
|
||||
action: 'email',
|
||||
})
|
||||
await createWatchlistTrigger(watchList!.token, {
|
||||
watchList: watchList!['@id'],
|
||||
event,
|
||||
action: 'chat',
|
||||
})
|
||||
setTriggersLoading(false)
|
||||
}
|
||||
|
||||
const removeTrigger = async (event: string) => {
|
||||
if (isCreation) return
|
||||
|
||||
setTriggersLoading(true)
|
||||
await deleteWatchlistTrigger(watchList!.token, {
|
||||
watchList: watchList!['@id'],
|
||||
event,
|
||||
action: 'email',
|
||||
})
|
||||
await deleteWatchlistTrigger(watchList!.token, {
|
||||
watchList: watchList!['@id'],
|
||||
event,
|
||||
action: 'chat',
|
||||
})
|
||||
setTriggersLoading(false)
|
||||
const domainStatusTagRenderer: TagRender = ({value, closable, onClose}: {
|
||||
value: EventAction
|
||||
closable: boolean
|
||||
onClose: () => void
|
||||
}) => {
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
title={rdapDomainStatusCodeDetailTranslated[value as keyof typeof rdapDomainStatusCodeDetailTranslated] || undefined}
|
||||
>
|
||||
<Tag
|
||||
color={eppStatusCodeToColor(value)}
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{marginInlineEnd: 4}}
|
||||
>
|
||||
{value}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -102,7 +97,10 @@ export function WatchlistForm({form, connectors, onFinish, isCreation, watchList
|
||||
{...formItemLayoutWithOutLabel}
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
initialValues={{triggers: ['last changed', 'transfer', 'expiration', 'deletion']}}
|
||||
initialValues={{
|
||||
trackedEvents: ['last changed', 'transfer', 'deletion'],
|
||||
trackedEppStatus: ['auto renew period', 'redemption period', 'pending delete', 'client hold', 'server hold']
|
||||
}}
|
||||
>
|
||||
|
||||
<Form.Item name='token' hidden>
|
||||
@ -191,8 +189,8 @@ export function WatchlistForm({form, connectors, onFinish, isCreation, watchList
|
||||
</Form.List>
|
||||
<Form.Item
|
||||
label={t`Tracked events`}
|
||||
name='triggers'
|
||||
rules={[{required: true, message: t`At least one trigger`, type: 'array'}]}
|
||||
name='trackedEvents'
|
||||
rules={[{required: true, message: t`At least one event`, type: 'array'}]}
|
||||
labelCol={{
|
||||
xs: {span: 24},
|
||||
sm: {span: 4}
|
||||
@ -205,11 +203,8 @@ export function WatchlistForm({form, connectors, onFinish, isCreation, watchList
|
||||
>
|
||||
<Select
|
||||
mode='multiple'
|
||||
tagRender={triggerTagRenderer}
|
||||
tagRender={eventActionTagRenderer}
|
||||
style={{width: '100%'}}
|
||||
onSelect={createTrigger}
|
||||
onDeselect={removeTrigger}
|
||||
loading={triggersLoading}
|
||||
options={Object.keys(rdapEventNameTranslated).map(e => ({
|
||||
value: e,
|
||||
title: rdapEventDetailTranslated[e as keyof typeof rdapEventDetailTranslated] || undefined,
|
||||
@ -218,6 +213,32 @@ export function WatchlistForm({form, connectors, onFinish, isCreation, watchList
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t`Tracked EPP status`}
|
||||
name='trackedEppStatus'
|
||||
rules={[{required: true, message: t`At least one EPP status`, type: 'array'}]}
|
||||
labelCol={{
|
||||
xs: {span: 24},
|
||||
sm: {span: 4}
|
||||
}}
|
||||
wrapperCol={{
|
||||
md: {span: 12},
|
||||
sm: {span: 20}
|
||||
}}
|
||||
required
|
||||
>
|
||||
<Select
|
||||
mode='multiple'
|
||||
tagRender={domainStatusTagRenderer}
|
||||
style={{width: '100%'}}
|
||||
options={Object.keys(rdapDomainStatusCodeDetailTranslated).map(e => ({
|
||||
value: e,
|
||||
title: rdapDomainStatusCodeDetailTranslated[e as keyof typeof rdapDomainStatusCodeDetailTranslated] || undefined,
|
||||
label: e
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t`Connector`}
|
||||
name='connector'
|
||||
|
||||
@ -3,21 +3,21 @@ import type {Connector} from '../../../utils/api/connectors'
|
||||
import {WatchlistCard} from './WatchlistCard'
|
||||
import type {Watchlist} from '../../../utils/api'
|
||||
|
||||
export function WatchlistsList({watchlists, onDelete, onUpdateWatchlist, connectors}: {
|
||||
export function WatchlistsList({watchlists, onChange, onUpdateWatchlist, connectors}: {
|
||||
watchlists: Watchlist[]
|
||||
onDelete: () => void
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>
|
||||
onChange: () => void
|
||||
onUpdateWatchlist: (values: { domains: string[], trackedEvents: string[], trackedEppStatus: string[], token: string }) => Promise<void>
|
||||
connectors: Array<Connector & { id: string }>
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{watchlists.map(watchlist =>
|
||||
{[...watchlists.filter(w => w.enabled), ...watchlists.filter(w => !w.enabled)].map(watchlist =>
|
||||
<WatchlistCard
|
||||
key={watchlist.token}
|
||||
watchlist={watchlist}
|
||||
onUpdateWatchlist={onUpdateWatchlist}
|
||||
connectors={connectors}
|
||||
onDelete={onDelete}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -7,10 +7,10 @@ import type {Edge} from '@xyflow/react'
|
||||
|
||||
export function domainEntitiesToEdges(d: Domain, withRegistrar = false): Edge[] {
|
||||
const rdapRoleTranslated = rdapRoleTranslation()
|
||||
const sponsor = d.entities.find(e => !e.deleted && e.roles.includes('sponsor'))
|
||||
const sponsor = d.entities.find(e => e.deletedAt === undefined && e.roles.includes('sponsor'))
|
||||
return d.entities
|
||||
.filter(e =>
|
||||
!e.deleted &&
|
||||
e.deletedAt === undefined &&
|
||||
(withRegistrar || !e.roles.includes('registrar')) &&
|
||||
((sponsor == null) || !e.roles.includes('registrar') || e.roles.includes('sponsor'))
|
||||
)
|
||||
|
||||
@ -15,10 +15,10 @@ export const domainToNode = (d: Domain): Node => ({
|
||||
})
|
||||
|
||||
export const domainEntitiesToNode = (d: Domain, withRegistrar = false): Node[] => {
|
||||
const sponsor = d.entities.find(e => !e.deleted && e.roles.includes('sponsor'))
|
||||
const sponsor = d.entities.find(e => e.deletedAt === undefined && e.roles.includes('sponsor'))
|
||||
return d.entities
|
||||
.filter(e =>
|
||||
!e.deleted &&
|
||||
e.deletedAt === undefined &&
|
||||
(withRegistrar || !e.roles.includes('registrar')) &&
|
||||
((sponsor == null) || !e.roles.includes('registrar') || e.roles.includes('sponsor'))
|
||||
)
|
||||
|
||||
@ -9,12 +9,12 @@ import {getIcannAccreditations} from "../../utils/api/icann-accreditations"
|
||||
const {Text, Paragraph} = Typography
|
||||
|
||||
interface FiltersType {
|
||||
'icannAccreditation.status': 'Accredited' | 'Reserved' | 'Terminated',
|
||||
status: 'Accredited' | 'Reserved' | 'Terminated',
|
||||
}
|
||||
|
||||
function RegistrarListTable(filters: FiltersType) {
|
||||
interface TableRow {
|
||||
key: string
|
||||
key: number
|
||||
handle: number
|
||||
name: string
|
||||
}
|
||||
@ -26,9 +26,9 @@ function RegistrarListTable(filters: FiltersType) {
|
||||
getIcannAccreditations(params).then((data) => {
|
||||
setTotal(data['hydra:totalItems'])
|
||||
setDataTable(data['hydra:member'].map((accreditation: IcannAccreditation) => ({
|
||||
key: accreditation.handle,
|
||||
handle: parseInt(accreditation.handle),
|
||||
name: accreditation.icannAccreditation.registrarName
|
||||
key: accreditation.id,
|
||||
handle: accreditation.id,
|
||||
name: accreditation.registrarName
|
||||
})
|
||||
).sort((a, b) => a.handle - b.handle))
|
||||
})
|
||||
@ -76,17 +76,17 @@ export default function IcannRegistrarPage() {
|
||||
Accredited: <>
|
||||
<Text>{t`An accredited number means that ICANN's contract with the registrar is ongoing.`}</Text>
|
||||
<Divider/>
|
||||
<RegistrarListTable {...{'icannAccreditation.status': 'Accredited'}} />
|
||||
<RegistrarListTable status='Accredited' />
|
||||
</>,
|
||||
Reserved: <>
|
||||
<Text>{t`A reserved number can be used by TLD registries for specific operations.`}</Text>
|
||||
<Divider/>
|
||||
<RegistrarListTable {...{'icannAccreditation.status': 'Reserved'}} />
|
||||
<RegistrarListTable status='Reserved' />
|
||||
</>,
|
||||
Terminated: <>
|
||||
<Text>{t`A terminated number means that ICANN's contract with the registrar has been terminated.`}</Text>
|
||||
<Divider/>
|
||||
<RegistrarListTable {...{'icannAccreditation.status': 'Terminated'}} />
|
||||
<RegistrarListTable status='Terminated' />
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@ -1,54 +1,34 @@
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {Card, Divider, Flex, Form, message} from 'antd'
|
||||
import type {Watchlist, WatchlistTrigger} from '../../utils/api'
|
||||
import {Divider, Flex, Form, message} from 'antd'
|
||||
import type {Watchlist} from '../../utils/api'
|
||||
import {getWatchlists, postWatchlist, putWatchlist} from '../../utils/api'
|
||||
import type {AxiosError} from 'axios'
|
||||
import {t} from 'ttag'
|
||||
import {WatchlistForm} from '../../components/tracking/watchlist/WatchlistForm'
|
||||
import {WatchlistsList} from '../../components/tracking/watchlist/WatchlistsList'
|
||||
import type {Connector} from '../../utils/api/connectors'
|
||||
import { getConnectors} from '../../utils/api/connectors'
|
||||
|
||||
import {showErrorAPI} from '../../utils/functions/showErrorAPI'
|
||||
import {CreateWatchlistButton} from "../../components/tracking/watchlist/CreateWatchlistButton"
|
||||
|
||||
interface FormValuesType {
|
||||
name?: string
|
||||
domains: string[]
|
||||
triggers: string[]
|
||||
trackedEvents: string[]
|
||||
trackedEppStatus: string[]
|
||||
connector?: string
|
||||
dsn?: string[]
|
||||
}
|
||||
|
||||
const getRequestDataFromFormCreation = (values: FormValuesType) => {
|
||||
const domainsURI = values.domains.map(d => '/api/domains/' + d.toLowerCase())
|
||||
let triggers: WatchlistTrigger[] = values.triggers.map(t => ({event: t, action: 'email'}))
|
||||
|
||||
if (values.dsn !== undefined) {
|
||||
triggers = [...triggers, ...values.triggers.map((t): WatchlistTrigger => ({
|
||||
event: t,
|
||||
action: 'chat'
|
||||
}))]
|
||||
}
|
||||
|
||||
return {
|
||||
name: values.name,
|
||||
domains: domainsURI,
|
||||
triggers,
|
||||
const getRequestDataFromFormCreation = (values: FormValuesType) =>
|
||||
({ name: values.name,
|
||||
domains: values.domains.map(d => '/api/domains/' + d.toLowerCase()),
|
||||
trackedEvents: values.trackedEvents,
|
||||
trackedEppStatus: values.trackedEppStatus,
|
||||
connector: values.connector !== undefined ? ('/api/connectors/' + values.connector) : undefined,
|
||||
dsn: values.dsn
|
||||
}
|
||||
}
|
||||
|
||||
const getRequestDataFromFormUpdate = (values: FormValuesType) => {
|
||||
const domainsURI = values.domains.map(d => '/api/domains/' + d.toLowerCase())
|
||||
|
||||
return {
|
||||
name: values.name,
|
||||
domains: domainsURI,
|
||||
connector: values.connector !== undefined ? ('/api/connectors/' + values.connector) : undefined,
|
||||
dsn: values.dsn
|
||||
}
|
||||
}
|
||||
dsn: values.dsn,
|
||||
enabled: true
|
||||
})
|
||||
|
||||
export default function WatchlistPage() {
|
||||
const [form] = Form.useForm()
|
||||
@ -56,19 +36,17 @@ export default function WatchlistPage() {
|
||||
const [watchlists, setWatchlists] = useState<Watchlist[]>()
|
||||
const [connectors, setConnectors] = useState<Array<Connector & { id: string }>>()
|
||||
|
||||
const onCreateWatchlist = (values: FormValuesType) => {
|
||||
postWatchlist(getRequestDataFromFormCreation(values)).then(() => {
|
||||
const onCreateWatchlist = async (values: FormValuesType) => await postWatchlist(getRequestDataFromFormCreation(values)).then(() => {
|
||||
form.resetFields()
|
||||
refreshWatchlists()
|
||||
messageApi.success(t`Watchlist created !`)
|
||||
}).catch((e: AxiosError) => {
|
||||
showErrorAPI(e, messageApi)
|
||||
})
|
||||
}
|
||||
|
||||
const onUpdateWatchlist = async (values: FormValuesType & { token: string }) => await putWatchlist({
|
||||
token: values.token,
|
||||
...getRequestDataFromFormUpdate(values)
|
||||
...getRequestDataFromFormCreation(values)
|
||||
}
|
||||
).then(() => {
|
||||
refreshWatchlists()
|
||||
@ -96,18 +74,18 @@ export default function WatchlistPage() {
|
||||
return (
|
||||
<Flex gap='middle' align='center' justify='center' vertical>
|
||||
{contextHolder}
|
||||
<Card size='small' loading={connectors === undefined} title={t`Create a Watchlist`} style={{width: '100%'}}>
|
||||
{(connectors != null) &&
|
||||
<WatchlistForm form={form} onFinish={onCreateWatchlist} connectors={connectors} isCreation/>}
|
||||
</Card>
|
||||
<Divider/>
|
||||
|
||||
{(connectors != null) && (watchlists != null) && watchlists.length > 0 &&
|
||||
<WatchlistsList
|
||||
watchlists={watchlists}
|
||||
onDelete={refreshWatchlists}
|
||||
connectors={connectors}
|
||||
onUpdateWatchlist={onUpdateWatchlist}
|
||||
/>}
|
||||
<>
|
||||
<CreateWatchlistButton onUpdateWatchlist={onCreateWatchlist} connectors={connectors} />
|
||||
<Divider/>
|
||||
<WatchlistsList
|
||||
watchlists={watchlists}
|
||||
onChange={refreshWatchlists}
|
||||
connectors={connectors}
|
||||
onUpdateWatchlist={onUpdateWatchlist}
|
||||
/>
|
||||
</>}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ interface IcannAccreditationList {
|
||||
|
||||
export async function getIcannAccreditations(params: object): Promise<IcannAccreditationList> {
|
||||
return (await request<IcannAccreditationList>({
|
||||
url: 'entities/icann-accreditations',
|
||||
url: 'icann-accreditations',
|
||||
params
|
||||
})).data
|
||||
}
|
||||
|
||||
@ -16,8 +16,6 @@ export type EventAction =
|
||||
| 'enum validation expiration'
|
||||
| string
|
||||
|
||||
export type TriggerAction = 'email' | 'chat'
|
||||
|
||||
export interface Event {
|
||||
action: EventAction
|
||||
date: string
|
||||
@ -32,6 +30,11 @@ export interface Entity {
|
||||
string,
|
||||
string | string[],
|
||||
]>] | []
|
||||
remarks?: {
|
||||
type: string
|
||||
description: string
|
||||
}[]
|
||||
icannAccreditation?: IcannAccreditation
|
||||
}
|
||||
|
||||
export interface Nameserver {
|
||||
@ -59,7 +62,7 @@ export interface Domain {
|
||||
entity: Entity
|
||||
events: Event[]
|
||||
roles: string[]
|
||||
deleted: boolean
|
||||
deletedAt?: string
|
||||
}>
|
||||
nameservers: Nameserver[]
|
||||
tld: Tld
|
||||
@ -74,18 +77,14 @@ export interface User {
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
export interface WatchlistTrigger {
|
||||
event: EventAction
|
||||
action: TriggerAction
|
||||
watchList?: string
|
||||
}
|
||||
|
||||
export interface WatchlistRequest {
|
||||
name?: string
|
||||
domains: string[]
|
||||
triggers?: Array<WatchlistTrigger>
|
||||
trackedEvents?: string[]
|
||||
trackedEppStatus?: string[]
|
||||
connector?: string
|
||||
dsn?: string[]
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export interface Watchlist {
|
||||
@ -93,7 +92,8 @@ export interface Watchlist {
|
||||
name?: string
|
||||
token: string
|
||||
domains: Domain[]
|
||||
triggers?: Array<WatchlistTrigger>
|
||||
trackedEvents?: string[]
|
||||
trackedEppStatus?: string[]
|
||||
dsn?: string[]
|
||||
connector?: {
|
||||
id: string
|
||||
@ -101,6 +101,7 @@ export interface Watchlist {
|
||||
createdAt: string
|
||||
}
|
||||
createdAt: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface InstanceConfig {
|
||||
@ -125,13 +126,11 @@ export interface TrackedDomains {
|
||||
}
|
||||
|
||||
export interface IcannAccreditation {
|
||||
handle: string
|
||||
icannAccreditation: {
|
||||
registrarName: string
|
||||
status: string
|
||||
date?: string
|
||||
updated?: string
|
||||
}
|
||||
id: number
|
||||
registrarName: string
|
||||
status: string
|
||||
date?: string
|
||||
updated?: string
|
||||
}
|
||||
|
||||
export async function request<T = object, R = AxiosResponse<T>, D = object>(config: AxiosRequestConfig): Promise<R> {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type {TrackedDomains, Watchlist, WatchlistRequest, WatchlistTrigger} from './index'
|
||||
import type {TrackedDomains, Watchlist, WatchlistRequest} from './index'
|
||||
import {request} from './index'
|
||||
|
||||
interface WatchlistList {
|
||||
@ -32,6 +32,18 @@ export async function postWatchlist(watchlist: WatchlistRequest) {
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function patchWatchlist(token: string, watchlist: Partial<WatchlistRequest>) {
|
||||
const response = await request<{ token: string }>({
|
||||
method: 'PATCH',
|
||||
url: 'watchlists/' + token,
|
||||
data: watchlist,
|
||||
headers: {
|
||||
'Content-Type': 'application/merge-patch+json'
|
||||
}
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function deleteWatchlist(token: string): Promise<void> {
|
||||
await request({
|
||||
method: 'DELETE',
|
||||
@ -56,20 +68,3 @@ export async function getTrackedDomainList(params: { page: number, itemsPerPage:
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function createWatchlistTrigger(watchListToken: string, watchListTrigger: WatchlistTrigger): Promise<WatchlistTrigger> {
|
||||
const response = await request<WatchlistTrigger>({
|
||||
method: 'POST',
|
||||
url: `watchlist-triggers`,
|
||||
data: watchListTrigger,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function deleteWatchlistTrigger(watchListToken: string, watchListTrigger: WatchlistTrigger): Promise<void> {
|
||||
await request<void>({
|
||||
method: 'DELETE',
|
||||
url: `watchlists/${watchListToken}/triggers/${watchListTrigger.action}/${watchListTrigger.event}`,
|
||||
data: watchListTrigger
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
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'
|
||||
export const eppStatusCodeToColor = (s?: string) =>
|
||||
s === undefined ? 'default' :
|
||||
['active', 'ok'].includes(s)
|
||||
? 'green'
|
||||
: ['pending delete', 'redemption period'].includes(s)
|
||||
? 'red'
|
||||
: s.startsWith('client')
|
||||
? 'purple'
|
||||
: s.startsWith('server') ? 'geekblue' : 'blue'
|
||||
|
||||
@ -68,23 +68,7 @@ export const rdapEventDetailTranslation = () => ({
|
||||
'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.`,
|
||||
|
||||
export const rdapDomainStatusCodeDetailTranslation = () => ({
|
||||
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.`,
|
||||
@ -110,6 +94,34 @@ export const rdapStatusCodeDetailTranslation = () => ({
|
||||
'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.`,
|
||||
|
||||
})
|
||||
|
||||
/**
|
||||
* @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.`,
|
||||
|
||||
...rdapDomainStatusCodeDetailTranslation(),
|
||||
|
||||
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.`
|
||||
})
|
||||
|
||||
|
||||
export const icannAccreditationTranslation = () => ({
|
||||
Terminated: t`Terminated`,
|
||||
Accredited: t`Accredited`,
|
||||
Reserved: t`Reserved`
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type {Domain} from '../api'
|
||||
|
||||
export const sortDomainEntities = (domain: Domain) => domain.entities
|
||||
.filter(e => !e.deleted)
|
||||
.filter(e => e.deletedAt === undefined)
|
||||
.sort((e1, e2) => {
|
||||
const p = (r: string[]) => r.includes('registrant')
|
||||
? 5
|
||||
|
||||
103
composer.json
103
composer.json
@ -20,7 +20,7 @@
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"php": ">=8.4",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"ext-simplexml": "*",
|
||||
@ -42,53 +42,53 @@
|
||||
"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/asset": "7.3.*",
|
||||
"symfony/asset-mapper": "7.3.*",
|
||||
"symfony/cache": "7.3.*",
|
||||
"symfony/console": "7.3.*",
|
||||
"symfony/discord-notifier": "7.3.*",
|
||||
"symfony/doctrine-messenger": "7.3.*",
|
||||
"symfony/dotenv": "7.3.*",
|
||||
"symfony/engagespot-notifier": "7.3.*",
|
||||
"symfony/expression-language": "7.3.*",
|
||||
"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/form": "7.3.*",
|
||||
"symfony/framework-bundle": "7.3.*",
|
||||
"symfony/google-chat-notifier": "7.3.*",
|
||||
"symfony/http-client": "7.3.*",
|
||||
"symfony/intl": "7.3.*",
|
||||
"symfony/lock": "7.3.*",
|
||||
"symfony/mailer": "7.3.*",
|
||||
"symfony/mattermost-notifier": "7.3.*",
|
||||
"symfony/microsoft-teams-notifier": "7.3.*",
|
||||
"symfony/mime": "7.3.*",
|
||||
"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/notifier": "7.3.*",
|
||||
"symfony/ntfy-notifier": "7.3.*",
|
||||
"symfony/process": "7.3.*",
|
||||
"symfony/property-access": "7.3.*",
|
||||
"symfony/property-info": "7.3.*",
|
||||
"symfony/pushover-notifier": "7.3.*",
|
||||
"symfony/rate-limiter": "7.3.*",
|
||||
"symfony/redis-messenger": "7.3.*",
|
||||
"symfony/rocket-chat-notifier": "7.3.*",
|
||||
"symfony/runtime": "7.3.*",
|
||||
"symfony/scheduler": "7.3.*",
|
||||
"symfony/security-bundle": "7.3.*",
|
||||
"symfony/serializer": "7.3.*",
|
||||
"symfony/slack-notifier": "7.3.*",
|
||||
"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.*",
|
||||
"symfony/string": "7.3.*",
|
||||
"symfony/telegram-notifier": "7.3.*",
|
||||
"symfony/translation": "7.3.*",
|
||||
"symfony/twig-bundle": "7.3.*",
|
||||
"symfony/uid": "7.3.*",
|
||||
"symfony/ux-turbo": "^2.18",
|
||||
"symfony/validator": "7.1.*",
|
||||
"symfony/web-link": "7.1.*",
|
||||
"symfony/validator": "7.3.*",
|
||||
"symfony/web-link": "7.3.*",
|
||||
"symfony/webpack-encore-bundle": "^2.1",
|
||||
"symfony/yaml": "7.1.*",
|
||||
"symfony/zulip-notifier": "7.1.*",
|
||||
"symfony/yaml": "7.3.*",
|
||||
"symfony/zulip-notifier": "7.3.*",
|
||||
"symfonycasts/verify-email-bundle": "*",
|
||||
"twig/extra-bundle": "^2.12|^3.0",
|
||||
"twig/twig": "^2.12|^3.0"
|
||||
@ -140,20 +140,23 @@
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "7.1.*",
|
||||
"require": "7.3.*",
|
||||
"docker": true
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/doctrine-fixtures-bundle": "^4.2",
|
||||
"friendsofphp/php-cs-fixer": "^3.61",
|
||||
"justinrainbow/json-schema": "^6.6",
|
||||
"phpstan/phpstan": "^1.11",
|
||||
"phpunit/phpunit": "^10",
|
||||
"symfony/browser-kit": "7.1.*",
|
||||
"symfony/css-selector": "7.1.*",
|
||||
"symfony/debug-bundle": "7.1.*",
|
||||
"symfony/browser-kit": "7.3.*",
|
||||
"symfony/css-selector": "7.3.*",
|
||||
"symfony/debug-bundle": "7.3.*",
|
||||
"symfony/maker-bundle": "^1.0",
|
||||
"symfony/phpunit-bridge": "^7.1",
|
||||
"symfony/stopwatch": "7.1.*",
|
||||
"symfony/web-profiler-bundle": "7.1.*"
|
||||
"symfony/phpunit-bridge": "^7.3",
|
||||
"symfony/stopwatch": "7.3.*",
|
||||
"symfony/web-profiler-bundle": "7.3.*",
|
||||
"zenstruck/foundry": "^2.7"
|
||||
}
|
||||
}
|
||||
|
||||
2099
composer.lock
generated
2099
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -19,4 +19,6 @@ return [
|
||||
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
||||
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||
SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||
Zenstruck\Foundry\ZenstruckFoundryBundle::class => ['dev' => true, 'test' => true],
|
||||
];
|
||||
|
||||
@ -27,4 +27,23 @@ api_platform:
|
||||
JWT:
|
||||
name: Authorization
|
||||
type: header
|
||||
exception_to_status:
|
||||
# The 4 following handlers are registered by default, keep those lines to prevent unexpected side effects
|
||||
Symfony\Component\Serializer\Exception\ExceptionInterface: 400 # Use a raw status code (recommended)
|
||||
ApiPlatform\Exception\InvalidArgumentException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST
|
||||
ApiPlatform\ParameterValidator\Exception\ValidationExceptionInterface: 400
|
||||
Doctrine\ORM\OptimisticLockException: 409
|
||||
|
||||
# Validation exception
|
||||
ApiPlatform\Validator\Exception\ValidationException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_UNPROCESSABLE_ENTITY
|
||||
|
||||
App\Exception\DomainNotFoundException: 404
|
||||
App\Exception\MalformedDomainException: 400
|
||||
App\Exception\TldNotSupportedException: 400
|
||||
App\Exception\UnknownRdapServerException: 400
|
||||
App\Exception\UnsupportedDsnScheme: 400
|
||||
|
||||
# Provider exception
|
||||
App\Exception\Provider\UserNoExplicitConsentException: 451
|
||||
App\Exception\Provider\AbstractProviderException: 400
|
||||
Metaregistrar\EPP\eppException: 400
|
||||
|
||||
11
config/packages/csrf.yaml
Normal file
11
config/packages/csrf.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
# Enable stateless CSRF protection for forms and logins/logouts
|
||||
framework:
|
||||
form:
|
||||
csrf_protection:
|
||||
token_id: submit
|
||||
|
||||
csrf_protection:
|
||||
stateless_token_ids:
|
||||
- submit
|
||||
- authenticate
|
||||
- logout
|
||||
@ -10,7 +10,7 @@ doctrine:
|
||||
use_savepoints: true
|
||||
orm:
|
||||
auto_generate_proxy_classes: true
|
||||
enable_lazy_ghost_objects: true
|
||||
enable_native_lazy_objects: true
|
||||
report_fields_where_declared: true
|
||||
validate_xml_mapping: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
|
||||
@ -23,7 +23,7 @@ framework:
|
||||
Symfony\Component\Notifier\Message\SmsMessage: async
|
||||
|
||||
App\Message\OrderDomain: async
|
||||
App\Message\ProcessWatchListsTrigger: async
|
||||
App\Message\ProcessWatchlistTrigger: async
|
||||
App\Message\SendDomainEventNotif: async
|
||||
App\Message\UpdateDomainsFromWatchlist: async
|
||||
App\Message\UpdateRdapServers: async
|
||||
|
||||
3
config/packages/property_info.yaml
Normal file
3
config/packages/property_info.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
framework:
|
||||
property_info:
|
||||
with_constructor_extractor: true
|
||||
@ -17,7 +17,7 @@ framework:
|
||||
|
||||
user_register:
|
||||
policy: token_bucket
|
||||
limit: 1
|
||||
limit: 5
|
||||
rate: { interval: '5 minutes' }
|
||||
|
||||
rdap_requests:
|
||||
|
||||
16
config/packages/zenstruck_foundry.yaml
Normal file
16
config/packages/zenstruck_foundry.yaml
Normal file
@ -0,0 +1,16 @@
|
||||
when@dev: &dev
|
||||
# See full configuration: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#full-default-bundle-configuration
|
||||
zenstruck_foundry:
|
||||
persistence:
|
||||
# Flush only once per call of `PersistentObjectFactory::create()`
|
||||
flush_once: true
|
||||
|
||||
# If you use the `make:factory --test` command, you may need to uncomment the following.
|
||||
# See https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#generate
|
||||
#services:
|
||||
# App\Tests\Factory\:
|
||||
# resource: '%kernel.project_dir%/tests/Factory/'
|
||||
# autowire: true
|
||||
# autoconfigure: true
|
||||
|
||||
when@test: *dev
|
||||
@ -46,3 +46,11 @@ services:
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
|
||||
when@test:
|
||||
parameters:
|
||||
gandi_pat_token: '%env(string:GANDI_PAT_TOKEN)%'
|
||||
namecom_username: '%env(string:NAMECOM_USERNAME)%'
|
||||
namecom_password: '%env(string:NAMECOM_PASSWORD)%'
|
||||
namecheap_username: '%env(string:NAMECHEAP_USERNAME)%'
|
||||
namecheap_token: '%env(string:NAMECHEAP_TOKEN)%'
|
||||
|
||||
@ -1,42 +1,47 @@
|
||||
# Please see https://github.com/maelgangloff/domain-watchdog
|
||||
|
||||
services:
|
||||
domainwatchdog:
|
||||
image: maelgangloff/domain-watchdog:latest
|
||||
container_name: domainwatchdog_app
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env.local
|
||||
environment:
|
||||
APP_ENV: prod
|
||||
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}
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-15}&charset=${POSTGRES_CHARSET:-utf8}
|
||||
MESSENGER_TRANSPORT_DSN: redis://valkey:6379/messages
|
||||
volumes:
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
- ./public/content:/app/public/content
|
||||
ports:
|
||||
- "127.0.0.1:8080:80"
|
||||
depends_on:
|
||||
- database
|
||||
|
||||
php-worker:
|
||||
image: maelgangloff/domain-watchdog:latest
|
||||
container_name: domainwatchdog_worker
|
||||
restart: always
|
||||
command: php /app/bin/console messenger:consume --all --time-limit=3600 -vvv
|
||||
env_file:
|
||||
- .env.local
|
||||
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}
|
||||
APP_ENV: prod
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-15}&charset=${POSTGRES_CHARSET:-utf8}
|
||||
MESSENGER_TRANSPORT_DSN: redis://valkey:6379/messages
|
||||
depends_on:
|
||||
- database
|
||||
healthcheck:
|
||||
disable: true
|
||||
test: [ ]
|
||||
# volumes:
|
||||
# - ./custom_rdap_servers.yaml:/app/config/app/custom_rdap_servers.yaml # Please see #41 issue
|
||||
disable: true
|
||||
|
||||
database:
|
||||
image: postgres:${POSTGRES_VERSION:-16}-alpine
|
||||
container_name: domainwatchdog_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-app}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
|
||||
@ -49,25 +54,23 @@ services:
|
||||
volumes:
|
||||
- database_data:/var/lib/postgresql/data:rw
|
||||
|
||||
# keydb:
|
||||
# image: eqalpha/keydb:latest
|
||||
# container_name: keydb
|
||||
# restart: always
|
||||
# ports:
|
||||
# - "127.0.0.1:6379:6379"
|
||||
valkey:
|
||||
image: valkey/valkey
|
||||
container_name: valkey
|
||||
restart: always
|
||||
|
||||
# influxdb2:
|
||||
# image: influxdb:2
|
||||
# ports:
|
||||
# - "127.0.0.1:8086:8086"
|
||||
# environment:
|
||||
# DOCKER_INFLUXDB_INIT_MODE: setup
|
||||
# DOCKER_INFLUXDB_INIT_USERNAME: USERNAME # Please modify
|
||||
# DOCKER_INFLUXDB_INIT_PASSWORD: PASSWORD # Please modify
|
||||
# DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: my-super-secret-auth-token # Please modify
|
||||
# DOCKER_INFLUXDB_INIT_RETENTION: 0
|
||||
# DOCKER_INFLUXDB_INIT_ORG: domainwatchdog
|
||||
# DOCKER_INFLUXDB_INIT_BUCKET: domainwatchdog
|
||||
# influxdb2:
|
||||
# image: influxdb:2
|
||||
# ports:
|
||||
# - "127.0.0.1:8086:8086"
|
||||
# environment:
|
||||
# DOCKER_INFLUXDB_INIT_MODE: setup
|
||||
# DOCKER_INFLUXDB_INIT_USERNAME: USERNAME # Please modify
|
||||
# DOCKER_INFLUXDB_INIT_PASSWORD: PASSWORD # Please modify
|
||||
# DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: my-super-secret-auth-token # Please modify
|
||||
# DOCKER_INFLUXDB_INIT_RETENTION: 0
|
||||
# DOCKER_INFLUXDB_INIT_ORG: domainwatchdog
|
||||
# DOCKER_INFLUXDB_INIT_BUCKET: domainwatchdog
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
|
||||
@ -55,9 +55,6 @@ if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then
|
||||
|
||||
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 "$@"
|
||||
|
||||
61
migrations/Version20250915192826.php
Normal file
61
migrations/Version20250915192826.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?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 Version20250915192826 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Move ICANN accreditation to table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE SEQUENCE icann_accreditation_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE TABLE icann_accreditation (id INT NOT NULL, registrar_name VARCHAR(255) DEFAULT NULL, rdap_base_url VARCHAR(255) DEFAULT NULL, status VARCHAR(255) DEFAULT NULL, updated DATE DEFAULT NULL, date DATE DEFAULT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('COMMENT ON COLUMN icann_accreditation.updated IS \'(DC2Type:date_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN icann_accreditation.date IS \'(DC2Type:date_immutable)\'');
|
||||
$this->addSql('ALTER TABLE entity ADD icann_accreditation_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE entity DROP icann_registrar_name');
|
||||
$this->addSql('ALTER TABLE entity DROP icann_rdap_base_url');
|
||||
$this->addSql('ALTER TABLE entity DROP icann_status');
|
||||
$this->addSql('ALTER TABLE entity DROP icann_updated');
|
||||
$this->addSql('ALTER TABLE entity DROP icann_date');
|
||||
|
||||
$this->addSql('DELETE FROM domain_entity de USING entity e WHERE de.entity_uid = e.id AND e.tld_id IS NULL');
|
||||
$this->addSql('DELETE FROM entity_event ee USING entity e WHERE ee.entity_uid = e.id AND e.tld_id IS NULL');
|
||||
$this->addSql('DELETE FROM nameserver_entity ne USING entity e WHERE ne.entity_uid = e.id AND e.tld_id IS NULL');
|
||||
$this->addSql('DELETE FROM entity e WHERE e.tld_id IS NULL;');
|
||||
|
||||
$this->addSql('ALTER TABLE entity ALTER tld_id SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE entity ADD CONSTRAINT FK_E284468D77C9FEB FOREIGN KEY (icann_accreditation_id) REFERENCES icann_accreditation (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_E284468D77C9FEB ON entity (icann_accreditation_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE entity DROP CONSTRAINT FK_E284468D77C9FEB');
|
||||
$this->addSql('DROP SEQUENCE icann_accreditation_id_seq CASCADE');
|
||||
$this->addSql('DROP TABLE icann_accreditation');
|
||||
$this->addSql('DROP INDEX IDX_E284468D77C9FEB');
|
||||
$this->addSql('ALTER TABLE entity ADD icann_registrar_name VARCHAR(255) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE entity ADD icann_rdap_base_url VARCHAR(255) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE entity ADD icann_status VARCHAR(255) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE entity ADD icann_updated DATE DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE entity ADD icann_date DATE DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE entity DROP icann_accreditation_id');
|
||||
$this->addSql('ALTER TABLE entity ALTER tld_id DROP NOT NULL');
|
||||
$this->addSql('COMMENT ON COLUMN entity.icann_updated IS \'(DC2Type:date_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN entity.icann_date IS \'(DC2Type:date_immutable)\'');
|
||||
}
|
||||
}
|
||||
31
migrations/Version20250915200844.php
Normal file
31
migrations/Version20250915200844.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?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 Version20250915200844 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Remove auto increment on id';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('DROP SEQUENCE icann_accreditation_id_seq CASCADE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE SEQUENCE icann_accreditation_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
}
|
||||
}
|
||||
35
migrations/Version20250915213341.php
Normal file
35
migrations/Version20250915213341.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?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 Version20250915213341 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'deleted_at on domain_entity';
|
||||
}
|
||||
|
||||
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_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||
$this->addSql('UPDATE domain_entity SET deleted_at = NOW() WHERE deleted IS TRUE');
|
||||
$this->addSql('ALTER TABLE domain_entity DROP deleted');
|
||||
$this->addSql('COMMENT ON COLUMN domain_entity.deleted_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_entity ADD deleted BOOLEAN NOT NULL DEFAULT FALSE');
|
||||
$this->addSql('ALTER TABLE domain_entity DROP deleted_at');
|
||||
}
|
||||
}
|
||||
40
migrations/Version20251004101245.php
Normal file
40
migrations/Version20251004101245.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?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 Version20251004101245 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// 1. Ajouter les nouvelles colonnes sans contrainte NOT NULL
|
||||
$this->addSql('ALTER TABLE "user" ADD created_at TIMESTAMP(0) WITHOUT TIME ZONE');
|
||||
$this->addSql('ALTER TABLE "user" ADD verified_at TIMESTAMP(0) WITHOUT TIME ZONE');
|
||||
$this->addSql('COMMENT ON COLUMN "user".created_at IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN "user".verified_at IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('UPDATE "user" SET created_at = TO_TIMESTAMP(0)');
|
||||
$this->addSql('UPDATE "user" SET verified_at = TO_TIMESTAMP(0) WHERE is_verified = true');
|
||||
$this->addSql('ALTER TABLE "user" ALTER COLUMN created_at SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE "user" DROP is_verified');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE "user" ADD is_verified BOOLEAN NOT NULL DEFAULT TRUE');
|
||||
$this->addSql('ALTER TABLE "user" DROP created_at');
|
||||
$this->addSql('ALTER TABLE "user" DROP verified_at');
|
||||
}
|
||||
}
|
||||
32
migrations/Version20251008094821.php
Normal file
32
migrations/Version20251008094821.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 Version20251008094821 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add deleted_at column on tld table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE tld ADD deleted_at DATE DEFAULT NULL');
|
||||
$this->addSql('COMMENT ON COLUMN tld.deleted_at IS \'(DC2Type:date_immutable)\'');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE tld DROP deleted_at');
|
||||
}
|
||||
}
|
||||
53
migrations/Version20251015165917.php
Normal file
53
migrations/Version20251015165917.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?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 Version20251015165917 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Remove watchlist_trigger and add tracked_events';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE watch_list ADD tracked_events JSONB');
|
||||
|
||||
$this->addSql("
|
||||
UPDATE watch_list wl
|
||||
SET tracked_events = sub.events::jsonb
|
||||
FROM (
|
||||
SELECT watch_list_id, json_agg(event) AS events
|
||||
FROM watch_list_trigger
|
||||
WHERE action = 'email'
|
||||
GROUP BY watch_list_id
|
||||
) AS sub
|
||||
WHERE wl.token = sub.watch_list_id
|
||||
");
|
||||
|
||||
$this->addSql("UPDATE watch_list SET tracked_events = '[]' WHERE tracked_events IS NULL");
|
||||
|
||||
$this->addSql('ALTER TABLE watch_list ALTER tracked_events SET NOT NULL');
|
||||
|
||||
$this->addSql('ALTER TABLE watch_list_trigger DROP CONSTRAINT fk_cf857a4cc4508918');
|
||||
$this->addSql('DROP TABLE watch_list_trigger');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE watch_list_trigger (event VARCHAR(255) NOT NULL, action VARCHAR(255) NOT NULL, watch_list_id UUID NOT NULL, PRIMARY KEY(event, watch_list_id, action))');
|
||||
$this->addSql('CREATE INDEX idx_cf857a4cc4508918 ON watch_list_trigger (watch_list_id)');
|
||||
$this->addSql('COMMENT ON COLUMN watch_list_trigger.watch_list_id IS \'(DC2Type:uuid)\'');
|
||||
$this->addSql('ALTER TABLE watch_list_trigger ADD CONSTRAINT fk_cf857a4cc4508918 FOREIGN KEY (watch_list_id) REFERENCES watch_list (token) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
|
||||
$this->addSql('ALTER TABLE watch_list DROP tracked_events');
|
||||
}
|
||||
}
|
||||
204
migrations/Version20251016193639.php
Normal file
204
migrations/Version20251016193639.php
Normal file
@ -0,0 +1,204 @@
|
||||
<?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 Version20251016193639 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Convert domain status to JSONB';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// domain.status
|
||||
$this->addSql("ALTER TABLE domain ADD status_jsonb JSONB DEFAULT '[]'::jsonb");
|
||||
$this->addSql("
|
||||
UPDATE domain
|
||||
SET status_jsonb = to_jsonb(string_to_array(status, ','))
|
||||
WHERE status IS NOT NULL AND status <> ''
|
||||
");
|
||||
$this->addSql('ALTER TABLE domain DROP COLUMN status');
|
||||
$this->addSql('ALTER TABLE domain RENAME COLUMN status_jsonb TO status');
|
||||
$this->addSql('COMMENT ON COLUMN domain.status IS NULL');
|
||||
|
||||
// domain_entity.roles
|
||||
$this->addSql("ALTER TABLE domain_entity ADD roles_jsonb JSONB DEFAULT '[]'::jsonb");
|
||||
$this->addSql("
|
||||
UPDATE domain_entity
|
||||
SET roles_jsonb = to_jsonb(string_to_array(roles, ','))
|
||||
WHERE roles IS NOT NULL AND roles <> ''
|
||||
");
|
||||
$this->addSql('ALTER TABLE domain_entity DROP COLUMN roles');
|
||||
$this->addSql('ALTER TABLE domain_entity RENAME COLUMN roles_jsonb TO roles');
|
||||
$this->addSql('COMMENT ON COLUMN domain_entity.roles IS NULL');
|
||||
|
||||
// nameserver_entity.roles
|
||||
$this->addSql("ALTER TABLE nameserver_entity ADD roles_jsonb JSONB DEFAULT '[]'::jsonb");
|
||||
$this->addSql("
|
||||
UPDATE nameserver_entity
|
||||
SET roles_jsonb = to_jsonb(string_to_array(roles, ','))
|
||||
WHERE roles IS NOT NULL AND roles <> ''
|
||||
");
|
||||
$this->addSql('ALTER TABLE nameserver_entity DROP COLUMN roles');
|
||||
$this->addSql('ALTER TABLE nameserver_entity RENAME COLUMN roles_jsonb TO roles');
|
||||
$this->addSql('COMMENT ON COLUMN nameserver_entity.roles IS NULL');
|
||||
|
||||
// nameserver_entity.status
|
||||
$this->addSql("ALTER TABLE nameserver_entity ADD status_jsonb JSONB DEFAULT '[]'::jsonb");
|
||||
$this->addSql("
|
||||
UPDATE nameserver_entity
|
||||
SET status_jsonb = to_jsonb(string_to_array(status, ','))
|
||||
WHERE status IS NOT NULL AND status <> ''
|
||||
");
|
||||
$this->addSql('ALTER TABLE nameserver_entity DROP COLUMN status');
|
||||
$this->addSql('ALTER TABLE nameserver_entity RENAME COLUMN status_jsonb TO status');
|
||||
$this->addSql('COMMENT ON COLUMN nameserver_entity.status IS NULL');
|
||||
|
||||
// domain_status.add_status
|
||||
$this->addSql("ALTER TABLE domain_status ADD add_status_jsonb JSONB DEFAULT '[]'::jsonb");
|
||||
$this->addSql("
|
||||
UPDATE domain_status
|
||||
SET add_status_jsonb = to_jsonb(string_to_array(add_status, ','))
|
||||
WHERE add_status IS NOT NULL AND add_status <> ''
|
||||
");
|
||||
$this->addSql('ALTER TABLE domain_status DROP COLUMN add_status');
|
||||
$this->addSql('ALTER TABLE domain_status RENAME COLUMN add_status_jsonb TO add_status');
|
||||
$this->addSql('COMMENT ON COLUMN domain_status.add_status IS NULL');
|
||||
|
||||
// domain_status.delete_status
|
||||
$this->addSql("ALTER TABLE domain_status ADD delete_status_jsonb JSONB DEFAULT '[]'::jsonb");
|
||||
$this->addSql("
|
||||
UPDATE domain_status
|
||||
SET delete_status_jsonb = to_jsonb(string_to_array(delete_status, ','))
|
||||
WHERE delete_status IS NOT NULL AND delete_status <> ''
|
||||
");
|
||||
$this->addSql('ALTER TABLE domain_status DROP COLUMN delete_status');
|
||||
$this->addSql('ALTER TABLE domain_status RENAME COLUMN delete_status_jsonb TO delete_status');
|
||||
$this->addSql('COMMENT ON COLUMN domain_status.delete_status IS NULL');
|
||||
|
||||
// watch_list.webhook_dsn
|
||||
$this->addSql("ALTER TABLE watch_list ADD webhook_dsn_jsonb JSONB DEFAULT '[]'::jsonb");
|
||||
$this->addSql("
|
||||
UPDATE watch_list
|
||||
SET webhook_dsn_jsonb = to_jsonb(string_to_array(webhook_dsn, ','))
|
||||
WHERE webhook_dsn IS NOT NULL AND webhook_dsn <> ''
|
||||
");
|
||||
$this->addSql('ALTER TABLE watch_list DROP COLUMN webhook_dsn');
|
||||
$this->addSql('ALTER TABLE watch_list RENAME COLUMN webhook_dsn_jsonb TO webhook_dsn');
|
||||
$this->addSql('COMMENT ON COLUMN watch_list.webhook_dsn IS NULL');
|
||||
|
||||
$this->addSql('ALTER TABLE domain ALTER status DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE domain_entity ALTER roles DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE domain_entity ALTER roles SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE domain_status ALTER add_status DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE domain_status ALTER delete_status DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE nameserver_entity ALTER roles DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE nameserver_entity ALTER roles SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE nameserver_entity ALTER status DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE nameserver_entity ALTER status SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE watch_list ALTER webhook_dsn DROP DEFAULT');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// domain.status
|
||||
$this->addSql('ALTER TABLE domain ADD status_text TEXT DEFAULT NULL');
|
||||
$this->addSql("
|
||||
UPDATE domain
|
||||
SET status_text = array_to_string(ARRAY(
|
||||
SELECT jsonb_array_elements_text(status)
|
||||
), ',')
|
||||
WHERE status IS NOT NULL
|
||||
");
|
||||
$this->addSql('ALTER TABLE domain DROP COLUMN status');
|
||||
$this->addSql('ALTER TABLE domain RENAME COLUMN status_text TO status');
|
||||
$this->addSql('COMMENT ON COLUMN domain.status IS NULL');
|
||||
|
||||
// domain_entity.roles
|
||||
$this->addSql('ALTER TABLE domain_entity ADD roles_text TEXT DEFAULT NULL');
|
||||
$this->addSql("
|
||||
UPDATE domain_entity
|
||||
SET roles_text = array_to_string(ARRAY(
|
||||
SELECT jsonb_array_elements_text(roles)
|
||||
), ',')
|
||||
WHERE roles IS NOT NULL
|
||||
");
|
||||
$this->addSql('ALTER TABLE domain_entity DROP COLUMN roles');
|
||||
$this->addSql('ALTER TABLE domain_entity RENAME COLUMN roles_text TO roles');
|
||||
$this->addSql('COMMENT ON COLUMN domain_entity.roles IS NULL');
|
||||
|
||||
// nameserver_entity.roles
|
||||
$this->addSql('ALTER TABLE nameserver_entity ADD roles_text TEXT DEFAULT NULL');
|
||||
$this->addSql("
|
||||
UPDATE nameserver_entity
|
||||
SET roles_text = array_to_string(ARRAY(
|
||||
SELECT jsonb_array_elements_text(roles)
|
||||
), ',')
|
||||
WHERE roles IS NOT NULL
|
||||
");
|
||||
$this->addSql('ALTER TABLE nameserver_entity DROP COLUMN roles');
|
||||
$this->addSql('ALTER TABLE nameserver_entity RENAME COLUMN roles_text TO roles');
|
||||
$this->addSql('COMMENT ON COLUMN nameserver_entity.roles IS NULL');
|
||||
|
||||
// nameserver_entity.status
|
||||
$this->addSql('ALTER TABLE nameserver_entity ADD status_text TEXT DEFAULT NULL');
|
||||
$this->addSql("
|
||||
UPDATE nameserver_entity
|
||||
SET status_text = array_to_string(ARRAY(
|
||||
SELECT jsonb_array_elements_text(status)
|
||||
), ',')
|
||||
WHERE status IS NOT NULL
|
||||
");
|
||||
$this->addSql('ALTER TABLE nameserver_entity DROP COLUMN status');
|
||||
$this->addSql('ALTER TABLE nameserver_entity RENAME COLUMN status_text TO status');
|
||||
$this->addSql('COMMENT ON COLUMN nameserver_entity.status IS NULL');
|
||||
|
||||
// domain_status.add_status
|
||||
$this->addSql('ALTER TABLE domain_status ADD add_status_text TEXT DEFAULT NULL');
|
||||
$this->addSql("
|
||||
UPDATE domain_status
|
||||
SET add_status_text = array_to_string(ARRAY(
|
||||
SELECT jsonb_array_elements_text(add_status)
|
||||
), ',')
|
||||
WHERE add_status IS NOT NULL
|
||||
");
|
||||
$this->addSql('ALTER TABLE domain_status DROP COLUMN add_status');
|
||||
$this->addSql('ALTER TABLE domain_status RENAME COLUMN add_status_text TO add_status');
|
||||
$this->addSql('COMMENT ON COLUMN domain_status.add_status IS NULL');
|
||||
|
||||
// domain_status.delete_status
|
||||
$this->addSql('ALTER TABLE domain_status ADD delete_status_text TEXT DEFAULT NULL');
|
||||
$this->addSql("
|
||||
UPDATE domain_status
|
||||
SET delete_status_text = array_to_string(ARRAY(
|
||||
SELECT jsonb_array_elements_text(delete_status)
|
||||
), ',')
|
||||
WHERE delete_status IS NOT NULL
|
||||
");
|
||||
$this->addSql('ALTER TABLE domain_status DROP COLUMN delete_status');
|
||||
$this->addSql('ALTER TABLE domain_status RENAME COLUMN delete_status_text TO delete_status');
|
||||
$this->addSql('COMMENT ON COLUMN domain_status.delete_status IS NULL');
|
||||
|
||||
// watch_list.webhook_dsn
|
||||
$this->addSql('ALTER TABLE watch_list ADD webhook_dsn_text TEXT DEFAULT NULL');
|
||||
$this->addSql("
|
||||
UPDATE watch_list
|
||||
SET webhook_dsn_text = array_to_string(ARRAY(
|
||||
SELECT jsonb_array_elements_text(webhook_dsn)
|
||||
), ',')
|
||||
WHERE webhook_dsn IS NOT NULL
|
||||
");
|
||||
$this->addSql('ALTER TABLE watch_list DROP COLUMN webhook_dsn');
|
||||
$this->addSql('ALTER TABLE watch_list RENAME COLUMN webhook_dsn_text TO webhook_dsn');
|
||||
$this->addSql('COMMENT ON COLUMN watch_list.webhook_dsn IS NULL');
|
||||
}
|
||||
}
|
||||
96
migrations/Version20251019120358.php
Normal file
96
migrations/Version20251019120358.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?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 Version20251019120358 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Lowercase on columns';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql("UPDATE domain
|
||||
SET status = (
|
||||
SELECT jsonb_agg(lower(value::text)::jsonb)
|
||||
FROM jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(status) = 'array' THEN status
|
||||
WHEN jsonb_typeof(status) = 'object' THEN to_jsonb(array(SELECT jsonb_array_elements_text(jsonb_agg(value)) FROM jsonb_each_text(status)))
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS t(value)
|
||||
)");
|
||||
|
||||
$this->addSql("UPDATE domain_status
|
||||
SET add_status = (
|
||||
SELECT jsonb_agg(lower(value::text)::jsonb)
|
||||
FROM jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(add_status) = 'array' THEN add_status
|
||||
WHEN jsonb_typeof(add_status) = 'object' THEN to_jsonb(array(SELECT jsonb_array_elements_text(jsonb_agg(value)) FROM jsonb_each_text(add_status)))
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS t(value)
|
||||
), delete_status = (
|
||||
SELECT jsonb_agg(lower(value::text)::jsonb)
|
||||
FROM jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(delete_status) = 'array' THEN delete_status
|
||||
WHEN jsonb_typeof(delete_status) = 'object' THEN to_jsonb(array(SELECT jsonb_array_elements_text(jsonb_agg(value)) FROM jsonb_each_text(delete_status)))
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS t(value)
|
||||
)");
|
||||
|
||||
$this->addSql("UPDATE domain_entity
|
||||
SET roles = (
|
||||
SELECT jsonb_agg(lower(value::text)::jsonb)
|
||||
FROM jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(roles) = 'array' THEN roles
|
||||
WHEN jsonb_typeof(roles) = 'object' THEN to_jsonb(array(SELECT jsonb_array_elements_text(jsonb_agg(value)) FROM jsonb_each_text(roles)))
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS t(value)
|
||||
)");
|
||||
|
||||
$this->addSql("UPDATE nameserver_entity
|
||||
SET roles = (
|
||||
SELECT jsonb_agg(lower(value::text)::jsonb)
|
||||
FROM jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(roles) = 'array' THEN roles
|
||||
WHEN jsonb_typeof(roles) = 'object' THEN to_jsonb(array(SELECT jsonb_array_elements_text(jsonb_agg(value)) FROM jsonb_each_text(roles)))
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS t(value)
|
||||
), status = (
|
||||
SELECT jsonb_agg(lower(value::text)::jsonb)
|
||||
FROM jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(status) = 'array' THEN status
|
||||
WHEN jsonb_typeof(status) = 'object' THEN to_jsonb(array(SELECT jsonb_array_elements_text(jsonb_agg(value)) FROM jsonb_each_text(status)))
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS t(value)
|
||||
)");
|
||||
|
||||
$this->addSql('UPDATE domain_event SET action = lower(action)');
|
||||
$this->addSql('UPDATE entity_event SET action = lower(action)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
}
|
||||
}
|
||||
31
migrations/Version20251019211214.php
Normal file
31
migrations/Version20251019211214.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?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 Version20251019211214 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add tracked_epp_status on watchlist';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql("ALTER TABLE watch_list ADD tracked_epp_status JSONB DEFAULT '[]'::jsonb");
|
||||
$this->addSql('ALTER TABLE watch_list ALTER tracked_epp_status DROP DEFAULT ');
|
||||
$this->addSql('ALTER TABLE watch_list ALTER tracked_epp_status SET NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE watch_list DROP tracked_epp_status');
|
||||
}
|
||||
}
|
||||
79
migrations/Version20251025152900.php
Normal file
79
migrations/Version20251025152900.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?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 Version20251025152900 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Rename WatchList to Watchlist';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE watch_lists_domains DROP CONSTRAINT fk_f693e1d0d52d7aa6');
|
||||
$this->addSql('ALTER TABLE watch_list DROP CONSTRAINT fk_152b584b4d085745');
|
||||
$this->addSql('ALTER TABLE watch_list DROP CONSTRAINT fk_152b584ba76ed395');
|
||||
|
||||
$this->addSql('ALTER TABLE watch_list RENAME TO watchlist');
|
||||
$this->addSql('ALTER INDEX idx_152b584ba76ed395 RENAME TO IDX_340388D3A76ED395');
|
||||
$this->addSql('ALTER INDEX idx_152b584b4d085745 RENAME TO IDX_340388D34D085745');
|
||||
|
||||
$this->addSql('ALTER TABLE watchlist ADD CONSTRAINT FK_340388D3A76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE watchlist ADD CONSTRAINT FK_340388D34D085745 FOREIGN KEY (connector_id) REFERENCES connector (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
|
||||
$this->addSql('ALTER TABLE watch_lists_domains ADD CONSTRAINT FK_F693E1D0D52D7AA6 FOREIGN KEY (watch_list_token) REFERENCES watchlist (token) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
|
||||
$this->addSql('ALTER TABLE watch_lists_domains DROP CONSTRAINT fk_f693e1d0af923913');
|
||||
$this->addSql('ALTER TABLE watch_lists_domains DROP CONSTRAINT fk_f693e1d0d52d7aa6');
|
||||
|
||||
$this->addSql('ALTER TABLE watch_lists_domains RENAME TO watchlist_domains');
|
||||
$this->addSql('ALTER INDEX idx_f693e1d0af923913 RENAME TO IDX_196DE762AF923913');
|
||||
$this->addSql('ALTER INDEX idx_f693e1d0d52d7aa6 RENAME TO IDX_196DE762F1E43AD7');
|
||||
|
||||
$this->addSql('ALTER TABLE watchlist_domains RENAME COLUMN watch_list_token TO watchlist_token');
|
||||
|
||||
$this->addSql('COMMENT ON COLUMN watchlist_domains.watchlist_token IS \'(DC2Type:uuid)\'');
|
||||
|
||||
$this->addSql('ALTER TABLE watchlist_domains ADD CONSTRAINT FK_196DE762F1E43AD7 FOREIGN KEY (watchlist_token) REFERENCES watchlist (token) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE watchlist_domains ADD CONSTRAINT FK_196DE762AF923913 FOREIGN KEY (domain_ldh_name) REFERENCES domain (ldh_name) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE watch_lists_domains DROP CONSTRAINT FK_F693E1D0D52D7AA6');
|
||||
$this->addSql('ALTER TABLE watchlist DROP CONSTRAINT FK_340388D3A76ED395');
|
||||
$this->addSql('ALTER TABLE watchlist DROP CONSTRAINT FK_340388D34D085745');
|
||||
|
||||
$this->addSql('ALTER TABLE watchlist RENAME TO watch_list');
|
||||
$this->addSql('ALTER INDEX IDX_340388D3A76ED395 RENAME TO idx_152b584ba76ed395');
|
||||
$this->addSql('ALTER INDEX IDX_340388D34D085745 RENAME TO idx_152b584b4d085745');
|
||||
|
||||
$this->addSql('ALTER TABLE watch_list ADD CONSTRAINT fk_152b584ba76ed395 FOREIGN KEY (user_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE watch_list ADD CONSTRAINT fk_152b584b4d085745 FOREIGN KEY (connector_id) REFERENCES connector (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
|
||||
$this->addSql('ALTER TABLE watch_lists_domains ADD CONSTRAINT fk_f693e1d0d52d7aa6 FOREIGN KEY (watch_list_token) REFERENCES watch_list (token) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
|
||||
$this->addSql('ALTER TABLE watchlist_domains DROP CONSTRAINT FK_196DE762F1E43AD7');
|
||||
$this->addSql('ALTER TABLE watchlist_domains DROP CONSTRAINT FK_196DE762AF923913');
|
||||
|
||||
$this->addSql('ALTER TABLE watchlist_domains RENAME COLUMN watchlist_token TO watch_list_token');
|
||||
|
||||
$this->addSql('ALTER TABLE watchlist_domains RENAME TO watch_lists_domains');
|
||||
$this->addSql('ALTER INDEX IDX_196DE762AF923913 RENAME TO idx_f693e1d0af923913');
|
||||
$this->addSql('ALTER INDEX IDX_196DE762F1E43AD7 RENAME TO idx_f693e1d0d52d7aa6');
|
||||
|
||||
$this->addSql('COMMENT ON COLUMN watch_lists_domains.watch_list_token IS \'(DC2Type:uuid)\'');
|
||||
|
||||
$this->addSql('ALTER TABLE watch_lists_domains ADD CONSTRAINT fk_f693e1d0af923913 FOREIGN KEY (domain_ldh_name) REFERENCES domain (ldh_name) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE watch_lists_domains ADD CONSTRAINT fk_f693e1d0d52d7aa6 FOREIGN KEY (watch_list_token) REFERENCES watchlist (token) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
}
|
||||
31
migrations/Version20251025160938.php
Normal file
31
migrations/Version20251025160938.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?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 Version20251025160938 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add enabled flag on watchlist';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE watchlist ADD enabled BOOLEAN NOT NULL DEFAULT true');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE watchlist DROP enabled');
|
||||
}
|
||||
}
|
||||
@ -6,3 +6,4 @@ parameters:
|
||||
- public/
|
||||
- src/
|
||||
- tests/
|
||||
- migrations/
|
||||
|
||||
@ -30,5 +30,6 @@
|
||||
</source>
|
||||
|
||||
<extensions>
|
||||
<bootstrap class="Zenstruck\Foundry\PHPUnit\FoundryExtension" />
|
||||
</extensions>
|
||||
</phpunit>
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Api\Extension;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
|
||||
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use App\Entity\Entity;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
class NotNullAccreditationIcannExtension implements QueryCollectionExtensionInterface
|
||||
{
|
||||
public function applyToCollection(
|
||||
QueryBuilder $queryBuilder,
|
||||
QueryNameGeneratorInterface $queryNameGenerator,
|
||||
string $resourceClass,
|
||||
?Operation $operation = null,
|
||||
array $context = [],
|
||||
): void {
|
||||
if (Entity::class !== $resourceClass) {
|
||||
return;
|
||||
}
|
||||
if ($operation && 'icann_accreditations_collection' === $operation->getName()) {
|
||||
$rootAlias = $queryBuilder->getRootAliases()[0];
|
||||
$queryBuilder->andWhere(sprintf('%s.icannAccreditation.status IS NOT NULL', $rootAlias));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Message\ProcessWatchListsTrigger;
|
||||
use App\Message\ProcessWatchlistTrigger;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
@ -12,10 +12,10 @@ use Symfony\Component\Messenger\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:process-watchlists',
|
||||
description: 'Process watchlists and send emails if necessary',
|
||||
name: 'app:process-watchlist',
|
||||
description: 'Process watchlist and send emails if necessary',
|
||||
)]
|
||||
class ProcessWatchlistsCommand extends Command
|
||||
class ProcessWatchlistCommand extends Command
|
||||
{
|
||||
public function __construct(private readonly MessageBusInterface $bus)
|
||||
{
|
||||
@ -33,7 +33,7 @@ class ProcessWatchlistsCommand extends Command
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$this->bus->dispatch(new ProcessWatchListsTrigger());
|
||||
$this->bus->dispatch(new ProcessWatchlistTrigger());
|
||||
|
||||
$io->success('Watchlist processing triggered!');
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\WatchList;
|
||||
use App\Entity\Watchlist;
|
||||
use App\Message\SendDomainEventNotif;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Service\RDAPService;
|
||||
@ -48,7 +48,7 @@ class RegisterDomainCommand extends Command
|
||||
|
||||
try {
|
||||
if (null !== $domain && !$force) {
|
||||
if (!$domain->isToBeUpdated(true, true)) {
|
||||
if (!$this->rdapService->isToBeUpdated($domain, true, true)) {
|
||||
$io->warning('The domain name is already present in the database and does not need to be updated at this time.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
@ -60,11 +60,11 @@ class RegisterDomainCommand extends Command
|
||||
|
||||
if ($notif) {
|
||||
$randomizer = new Randomizer();
|
||||
$watchLists = $randomizer->shuffleArray($domain->getWatchLists()->toArray());
|
||||
$watchlists = $randomizer->shuffleArray($domain->getWatchlists()->toArray());
|
||||
|
||||
/** @var WatchList $watchList */
|
||||
foreach ($watchLists as $watchList) {
|
||||
$this->bus->dispatch(new SendDomainEventNotif($watchList->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
/** @var Watchlist $watchlist */
|
||||
foreach ($watchlists as $watchlist) {
|
||||
$this->bus->dispatch(new SendDomainEventNotif($watchlist->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
|
||||
namespace App\Config;
|
||||
|
||||
use App\Service\Connector\AutodnsProvider;
|
||||
use App\Service\Connector\EppClientProvider;
|
||||
use App\Service\Connector\GandiProvider;
|
||||
use App\Service\Connector\NamecheapProvider;
|
||||
use App\Service\Connector\NameComProvider;
|
||||
use App\Service\Connector\OvhProvider;
|
||||
use App\Service\Provider\AutodnsProvider;
|
||||
use App\Service\Provider\EppClientProvider;
|
||||
use App\Service\Provider\GandiProvider;
|
||||
use App\Service\Provider\NamecheapProvider;
|
||||
use App\Service\Provider\NameComProvider;
|
||||
use App\Service\Provider\OvhProvider;
|
||||
|
||||
enum ConnectorProvider: string
|
||||
{
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Config;
|
||||
|
||||
enum TriggerAction: string
|
||||
{
|
||||
case SendEmail = 'email';
|
||||
case SendChat = 'chat';
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\WatchList;
|
||||
use App\Message\SendDomainEventNotif;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Service\RDAPService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Random\Randomizer;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Messenger\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\RateLimiter\RateLimiterFactory;
|
||||
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
|
||||
class DomainRefreshController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly DomainRepository $domainRepository,
|
||||
private readonly RDAPService $RDAPService,
|
||||
private readonly RateLimiterFactory $rdapRequestsLimiter,
|
||||
private readonly MessageBusInterface $bus,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly KernelInterface $kernel,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws DecodingExceptionInterface
|
||||
* @throws ExceptionInterface
|
||||
* @throws \Exception
|
||||
* @throws HttpExceptionInterface
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function __invoke(string $ldhName, Request $request): Domain
|
||||
{
|
||||
$idnDomain = RDAPService::convertToIdn($ldhName);
|
||||
$userId = $this->getUser()->getUserIdentifier();
|
||||
|
||||
$this->logger->info('User {username} wants to update the domain name {idnDomain}.', [
|
||||
'username' => $userId,
|
||||
'idnDomain' => $idnDomain,
|
||||
]);
|
||||
|
||||
/** @var ?Domain $domain */
|
||||
$domain = $this->domainRepository->findOneBy(['ldhName' => $idnDomain]);
|
||||
// If the domain name exists in the database, recently updated and not important, we return the stored Domain
|
||||
if (null !== $domain
|
||||
&& !$domain->getDeleted()
|
||||
&& !$domain->isToBeUpdated(true, true)
|
||||
&& !$this->kernel->isDebug()
|
||||
&& true !== filter_var($request->get('forced', false), FILTER_VALIDATE_BOOLEAN)
|
||||
) {
|
||||
$this->logger->info('It is not necessary to update the information of the domain name {idnDomain} with the RDAP protocol.', [
|
||||
'idnDomain' => $idnDomain,
|
||||
]);
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
if (false === $this->kernel->isDebug() && true === $this->getParameter('limited_features')) {
|
||||
$limiter = $this->rdapRequestsLimiter->create($userId);
|
||||
$limit = $limiter->consume();
|
||||
|
||||
if (!$limit->isAccepted()) {
|
||||
throw new TooManyRequestsHttpException($limit->getRetryAfter()->getTimestamp() - time());
|
||||
}
|
||||
}
|
||||
|
||||
$updatedAt = null === $domain ? new \DateTimeImmutable('now') : $domain->getUpdatedAt();
|
||||
$domain = $this->RDAPService->registerDomain($idnDomain);
|
||||
|
||||
$randomizer = new Randomizer();
|
||||
$watchLists = $randomizer->shuffleArray($domain->getWatchLists()->toArray());
|
||||
|
||||
/** @var WatchList $watchList */
|
||||
foreach ($watchLists as $watchList) {
|
||||
$this->bus->dispatch(new SendDomainEventNotif($watchList->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
}
|
||||
@ -5,15 +5,19 @@ namespace App\Controller;
|
||||
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
class HomeController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly RouterInterface $router)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RouterInterface $router,
|
||||
private readonly ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(path: '/', name: 'index')]
|
||||
@ -25,7 +29,10 @@ class HomeController extends AbstractController
|
||||
#[Route(path: '/login/oauth', name: 'oauth_connect')]
|
||||
public function connectAction(ClientRegistry $clientRegistry): Response
|
||||
{
|
||||
return $clientRegistry->getClient('oauth')->redirect([], []);
|
||||
if ($this->parameterBag->get('oauth_enabled')) {
|
||||
return $clientRegistry->getClient('oauth')->redirect([], []);
|
||||
}
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
#[Route(path: '/logout', name: 'logout')]
|
||||
|
||||
@ -21,6 +21,7 @@ use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\RateLimiter\RateLimiterFactory;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class RegistrationController extends AbstractController
|
||||
{
|
||||
@ -33,6 +34,7 @@ class RegistrationController extends AbstractController
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly KernelInterface $kernel,
|
||||
private readonly ValidatorInterface $validator,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -44,7 +46,7 @@ class RegistrationController extends AbstractController
|
||||
name: 'user_register',
|
||||
defaults: [
|
||||
'_api_resource_class' => User::class,
|
||||
'_api_operation_name' => 'register',
|
||||
'_api_operation_name' => 'user_register',
|
||||
],
|
||||
methods: ['POST']
|
||||
)]
|
||||
@ -64,22 +66,21 @@ class RegistrationController extends AbstractController
|
||||
}
|
||||
|
||||
$user = $this->serializer->deserialize($request->getContent(), User::class, 'json', ['groups' => 'user:register']);
|
||||
if (null === $user->getEmail() || null === $user->getPassword()) {
|
||||
throw new BadRequestHttpException('Bad request');
|
||||
$violations = $this->validator->validate($user);
|
||||
|
||||
if ($violations->count() > 0) {
|
||||
throw new BadRequestHttpException($violations->get(0));
|
||||
}
|
||||
|
||||
$user->setPassword(
|
||||
$userPasswordHasher->hashPassword(
|
||||
$user,
|
||||
$user->getPassword()
|
||||
$user->getPlainPassword()
|
||||
)
|
||||
);
|
||||
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
)->setCreatedAt(new \DateTimeImmutable());
|
||||
|
||||
if (false === (bool) $this->getParameter('registration_verify_email')) {
|
||||
$user->setVerified(true);
|
||||
$user->setVerifiedAt($user->getCreatedAt());
|
||||
} else {
|
||||
$email = $this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
|
||||
(new TemplatedEmail())
|
||||
@ -91,13 +92,16 @@ class RegistrationController extends AbstractController
|
||||
);
|
||||
|
||||
$signedUrl = (string) $email->getContext()['signedUrl'];
|
||||
$this->logger->notice('The validation link for user {username} is {signedUrl}', [
|
||||
$this->logger->notice('The validation link for this user is generated', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'signedUrl' => $signedUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->logger->info('A new user has registered ({username}).', [
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
|
||||
$this->logger->info('New user has registered', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
@ -121,7 +125,7 @@ class RegistrationController extends AbstractController
|
||||
|
||||
$this->emailVerifier->handleEmailConfirmation($request, $user);
|
||||
|
||||
$this->logger->info('User {username} has validated his email address.', [
|
||||
$this->logger->info('User has validated his email address', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\Statistics;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
@ -15,7 +15,7 @@ class StatisticsController extends AbstractController
|
||||
public function __construct(
|
||||
private readonly CacheItemPoolInterface $pool,
|
||||
private readonly DomainRepository $domainRepository,
|
||||
private readonly WatchListRepository $watchListRepository,
|
||||
private readonly WatchlistRepository $watchlistRepository,
|
||||
private readonly KernelInterface $kernel,
|
||||
) {
|
||||
}
|
||||
@ -34,22 +34,10 @@ class StatisticsController extends AbstractController
|
||||
->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])
|
||||
$this->getCachedItem('stats.domain.tracked', fn () => $this->watchlistRepository->getTrackedDomainCount())
|
||||
)
|
||||
->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())
|
||||
$this->getCachedItem('stats.domain.count', fn () => $this->domainRepository->getActiveDomainCountByTld())
|
||||
)
|
||||
->setDomainCountTotal(
|
||||
$this->getCachedItem('stats.domain.total', fn () => $this->domainRepository->count(['deleted' => false])
|
||||
|
||||
@ -6,8 +6,11 @@ use App\Entity\Domain;
|
||||
use App\Entity\DomainEvent;
|
||||
use App\Entity\DomainStatus;
|
||||
use App\Entity\User;
|
||||
use App\Entity\WatchList;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Entity\Watchlist;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use App\Service\CalendarService;
|
||||
use App\Service\RDAPService;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Eluceo\iCal\Domain\Entity\Calendar;
|
||||
use Eluceo\iCal\Presentation\Component\Property;
|
||||
@ -23,10 +26,13 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class WatchListController extends AbstractController
|
||||
class WatchlistController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly WatchListRepository $watchListRepository,
|
||||
private readonly WatchlistRepository $watchlistRepository,
|
||||
private readonly RDAPService $RDAPService,
|
||||
private readonly CalendarService $calendarService,
|
||||
private readonly DomainRepository $domainRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -34,17 +40,17 @@ class WatchListController extends AbstractController
|
||||
path: '/api/watchlists',
|
||||
name: 'watchlist_get_all_mine',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_resource_class' => Watchlist::class,
|
||||
'_api_operation_name' => 'get_all_mine',
|
||||
],
|
||||
methods: ['GET']
|
||||
)]
|
||||
public function getWatchLists(): Collection
|
||||
public function getWatchlists(): Collection
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
return $user->getWatchLists();
|
||||
return $user->getWatchlists();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,26 +63,26 @@ class WatchListController extends AbstractController
|
||||
path: '/api/watchlists/{token}/calendar',
|
||||
name: 'watchlist_calendar',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_resource_class' => Watchlist::class,
|
||||
'_api_operation_name' => 'calendar',
|
||||
]
|
||||
)]
|
||||
public function getWatchlistCalendar(string $token): Response
|
||||
{
|
||||
/** @var WatchList $watchList */
|
||||
$watchList = $this->watchListRepository->findOneBy(['token' => $token]);
|
||||
/** @var Watchlist $watchlist */
|
||||
$watchlist = $this->watchlistRepository->findOneBy(['token' => $token]);
|
||||
|
||||
$calendar = new Calendar();
|
||||
|
||||
/** @var Domain $domain */
|
||||
foreach ($watchList->getDomains()->getIterator() as $domain) {
|
||||
foreach ($domain->getDomainCalendarEvents() as $event) {
|
||||
foreach ($watchlist->getDomains()->getIterator() as $domain) {
|
||||
foreach ($this->calendarService->getDomainCalendarEvents($domain) as $event) {
|
||||
$calendar->addEvent($event);
|
||||
}
|
||||
}
|
||||
|
||||
$calendarResponse = (new CalendarFactory())->createCalendar($calendar);
|
||||
$calendarName = $watchList->getName();
|
||||
$calendarName = $watchlist->getName();
|
||||
if (null !== $calendarName) {
|
||||
$calendarResponse->withProperty(new Property('X-WR-CALNAME', new TextValue($calendarName)));
|
||||
}
|
||||
@ -93,7 +99,7 @@ class WatchListController extends AbstractController
|
||||
path: '/api/tracked',
|
||||
name: 'watchlist_get_tracked_domains',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_resource_class' => Watchlist::class,
|
||||
'_api_operation_name' => 'get_tracked_domains',
|
||||
]
|
||||
)]
|
||||
@ -102,18 +108,9 @@ class WatchListController extends AbstractController
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
$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 && !in_array($domain, $domains)) {
|
||||
$domains[] = $domain;
|
||||
}
|
||||
}
|
||||
$domains = $this->domainRepository->getMyTrackedDomains($user);
|
||||
foreach ($domains as $domain) {
|
||||
$domain->setExpiresInDays($this->RDAPService->getExpiresInDays($domain));
|
||||
}
|
||||
|
||||
usort($domains, fn (Domain $d1, Domain $d2) => $d1->getExpiresInDays() - $d2->getExpiresInDays());
|
||||
@ -128,14 +125,14 @@ class WatchListController extends AbstractController
|
||||
path: '/api/watchlists/{token}/rss/events',
|
||||
name: 'watchlist_rss_events',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_resource_class' => Watchlist::class,
|
||||
'_api_operation_name' => 'rss_events',
|
||||
]
|
||||
)]
|
||||
public function getWatchlistRssEventsFeed(string $token, Request $request): Response
|
||||
{
|
||||
/** @var WatchList $watchlist */
|
||||
$watchlist = $this->watchListRepository->findOneBy(['token' => $token]);
|
||||
/** @var Watchlist $watchlist */
|
||||
$watchlist = $this->watchlistRepository->findOneBy(['token' => $token]);
|
||||
|
||||
$feed = (new Feed())
|
||||
->setLanguage('en')
|
||||
@ -166,14 +163,14 @@ class WatchListController extends AbstractController
|
||||
path: '/api/watchlists/{token}/rss/status',
|
||||
name: 'watchlist_rss_status',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_resource_class' => Watchlist::class,
|
||||
'_api_operation_name' => 'rss_status',
|
||||
]
|
||||
)]
|
||||
public function getWatchlistRssStatusFeed(string $token, Request $request): Response
|
||||
{
|
||||
/** @var WatchList $watchlist */
|
||||
$watchlist = $this->watchListRepository->findOneBy(['token' => $token]);
|
||||
/** @var Watchlist $watchlist */
|
||||
$watchlist = $this->watchlistRepository->findOneBy(['token' => $token]);
|
||||
|
||||
$feed = (new Feed())
|
||||
->setLanguage('en')
|
||||
15
src/DataFixtures/AppFixtures.php
Normal file
15
src/DataFixtures/AppFixtures.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\DataFixtures;
|
||||
|
||||
use App\Story\DefaultUsersStory;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
class AppFixtures extends Fixture
|
||||
{
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
DefaultUsersStory::load();
|
||||
}
|
||||
}
|
||||
@ -27,7 +27,7 @@ use Symfony\Component\Uid\Uuid;
|
||||
),
|
||||
new Get(
|
||||
normalizationContext: ['groups' => 'connector:list'],
|
||||
security: 'object.user == user'
|
||||
security: 'object.getUser() == user'
|
||||
),
|
||||
new Post(
|
||||
normalizationContext: ['groups' => ['connector:create', 'connector:list']],
|
||||
@ -35,7 +35,7 @@ use Symfony\Component\Uid\Uuid;
|
||||
processor: ConnectorCreateProcessor::class
|
||||
),
|
||||
new Delete(
|
||||
security: 'object.user == user',
|
||||
security: 'object.getUser() == user',
|
||||
processor: ConnectorDeleteProcessor::class
|
||||
),
|
||||
]
|
||||
@ -61,10 +61,10 @@ class Connector
|
||||
private array $authData = [];
|
||||
|
||||
/**
|
||||
* @var Collection<int, WatchList>
|
||||
* @var Collection<int, Watchlist>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: WatchList::class, mappedBy: 'connector')]
|
||||
private Collection $watchLists;
|
||||
#[ORM\OneToMany(targetEntity: Watchlist::class, mappedBy: 'connector')]
|
||||
private Collection $watchlists;
|
||||
|
||||
#[Groups(['connector:list', 'watchlist:list'])]
|
||||
#[ORM\Column]
|
||||
@ -76,7 +76,7 @@ class Connector
|
||||
public function __construct()
|
||||
{
|
||||
$this->id = Uuid::v4();
|
||||
$this->watchLists = new ArrayCollection();
|
||||
$this->watchlists = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
@ -121,29 +121,29 @@ class Connector
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, WatchList>
|
||||
* @return Collection<int, Watchlist>
|
||||
*/
|
||||
public function getWatchLists(): Collection
|
||||
public function getWatchlists(): Collection
|
||||
{
|
||||
return $this->watchLists;
|
||||
return $this->watchlists;
|
||||
}
|
||||
|
||||
public function addWatchList(WatchList $watchList): static
|
||||
public function addWatchlist(Watchlist $watchlist): static
|
||||
{
|
||||
if (!$this->watchLists->contains($watchList)) {
|
||||
$this->watchLists->add($watchList);
|
||||
$watchList->setConnector($this);
|
||||
if (!$this->watchlists->contains($watchlist)) {
|
||||
$this->watchlists->add($watchlist);
|
||||
$watchlist->setConnector($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeWatchList(WatchList $watchList): static
|
||||
public function removeWatchlist(Watchlist $watchlist): static
|
||||
{
|
||||
if ($this->watchLists->removeElement($watchList)) {
|
||||
if ($this->watchlists->removeElement($watchlist)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($watchList->getConnector() === $this) {
|
||||
$watchList->setConnector(null);
|
||||
if ($watchlist->getConnector() === $this) {
|
||||
$watchlist->setConnector(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,6 +164,6 @@ class Connector
|
||||
|
||||
public function getWatchlistCount(): ?int
|
||||
{
|
||||
return $this->watchLists->count();
|
||||
return $this->watchlists->count();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,29 +2,21 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\QueryParameter;
|
||||
use App\Config\EventAction;
|
||||
use App\Controller\DomainRefreshController;
|
||||
use App\Exception\MalformedDomainException;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Service\RDAPService;
|
||||
use App\State\AutoRegisterDomainProvider;
|
||||
use App\State\FindDomainCollectionFromEntityProvider;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Eluceo\iCal\Domain\Entity\Attendee;
|
||||
use Eluceo\iCal\Domain\Entity\Event;
|
||||
use Eluceo\iCal\Domain\Enum\EventStatus;
|
||||
use Eluceo\iCal\Domain\ValueObject\Category;
|
||||
use Eluceo\iCal\Domain\ValueObject\Date;
|
||||
use Eluceo\iCal\Domain\ValueObject\EmailAddress;
|
||||
use Eluceo\iCal\Domain\ValueObject\SingleDay;
|
||||
use Eluceo\iCal\Domain\ValueObject\Timestamp;
|
||||
use Sabre\VObject\EofException;
|
||||
use Sabre\VObject\InvalidDataException;
|
||||
use Sabre\VObject\ParseException;
|
||||
use Sabre\VObject\Reader;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
|
||||
@ -40,9 +32,23 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
]
|
||||
),
|
||||
*/
|
||||
new GetCollection(
|
||||
uriTemplate: '/domains',
|
||||
normalizationContext: [
|
||||
'groups' => [
|
||||
'domain:list',
|
||||
'tld:list',
|
||||
'event:list',
|
||||
'event:list',
|
||||
],
|
||||
],
|
||||
provider: FindDomainCollectionFromEntityProvider::class,
|
||||
parameters: [
|
||||
'registrant' => new QueryParameter(description: 'The exact name of the registrant (case insensitive)', required: true),
|
||||
]
|
||||
),
|
||||
new Get(
|
||||
uriTemplate: '/domains/{ldhName}', // Do not delete this line, otherwise Symfony interprets the TLD of the domain name as a return type
|
||||
controller: DomainRefreshController::class,
|
||||
normalizationContext: [
|
||||
'groups' => [
|
||||
'domain:item',
|
||||
@ -54,7 +60,6 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
'ds:list',
|
||||
],
|
||||
],
|
||||
read: false
|
||||
),
|
||||
],
|
||||
provider: AutoRegisterDomainProvider::class
|
||||
@ -75,25 +80,30 @@ class Domain
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: DomainEvent::class, mappedBy: 'domain', cascade: ['persist'], orphanRemoval: true)]
|
||||
#[Groups(['domain:item', 'domain:list', 'watchlist:list'])]
|
||||
#[ApiProperty(
|
||||
openapiContext: [
|
||||
'type' => 'array',
|
||||
]
|
||||
)]
|
||||
private Collection $events;
|
||||
|
||||
/**
|
||||
* @var Collection<int, DomainEntity>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: DomainEntity::class, mappedBy: 'domain', cascade: ['persist'], orphanRemoval: true)]
|
||||
#[Groups(['domain:item'])]
|
||||
#[Groups(['domain:item', 'watchlist:item'])]
|
||||
#[SerializedName('entities')]
|
||||
private Collection $domainEntities;
|
||||
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)]
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['domain:item', 'domain:list', 'watchlist:item', 'watchlist:list'])]
|
||||
private array $status = [];
|
||||
|
||||
/**
|
||||
* @var Collection<int, WatchList>
|
||||
* @var Collection<int, Watchlist>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: WatchList::class, mappedBy: 'domains', cascade: ['persist'])]
|
||||
private Collection $watchLists;
|
||||
#[ORM\ManyToMany(targetEntity: Watchlist::class, mappedBy: 'domains', cascade: ['persist'])]
|
||||
private Collection $watchlists;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Nameserver>
|
||||
@ -103,7 +113,7 @@ class Domain
|
||||
joinColumns: [new ORM\JoinColumn(name: 'domain_ldh_name', referencedColumnName: 'ldh_name')],
|
||||
inverseJoinColumns: [new ORM\JoinColumn(name: 'nameserver_ldh_name', referencedColumnName: 'ldh_name')]
|
||||
)]
|
||||
#[Groups(['domain:item'])]
|
||||
#[Groups(['domain:item', 'watchlist:item'])]
|
||||
private Collection $nameservers;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
|
||||
@ -135,7 +145,7 @@ class Domain
|
||||
|
||||
#[ORM\Column(nullable: false, options: ['default' => false])]
|
||||
#[Groups(['domain:item', 'domain:list'])]
|
||||
private ?bool $delegationSigned = null;
|
||||
private bool $delegationSigned = false;
|
||||
|
||||
/**
|
||||
* @var Collection<int, DnsKey>
|
||||
@ -144,6 +154,8 @@ class Domain
|
||||
#[Groups(['domain:item'])]
|
||||
private Collection $dnsKey;
|
||||
|
||||
private ?int $expiresInDays;
|
||||
|
||||
private const IMPORTANT_EVENTS = [EventAction::Deletion->value, EventAction::Expiration->value];
|
||||
private const IMPORTANT_STATUS = [
|
||||
'redemption period',
|
||||
@ -160,7 +172,7 @@ class Domain
|
||||
{
|
||||
$this->events = new ArrayCollection();
|
||||
$this->domainEntities = new ArrayCollection();
|
||||
$this->watchLists = new ArrayCollection();
|
||||
$this->watchlists = new ArrayCollection();
|
||||
$this->nameservers = new ArrayCollection();
|
||||
$this->updatedAt = new \DateTimeImmutable('now');
|
||||
$this->createdAt = $this->updatedAt;
|
||||
@ -174,6 +186,9 @@ class Domain
|
||||
return $this->ldhName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MalformedDomainException
|
||||
*/
|
||||
public function setLdhName(string $ldhName): static
|
||||
{
|
||||
$this->ldhName = RDAPService::convertToIdn($ldhName);
|
||||
@ -266,27 +281,27 @@ class Domain
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, WatchList>
|
||||
* @return Collection<int, Watchlist>
|
||||
*/
|
||||
public function getWatchLists(): Collection
|
||||
public function getWatchlists(): Collection
|
||||
{
|
||||
return $this->watchLists;
|
||||
return $this->watchlists;
|
||||
}
|
||||
|
||||
public function addWatchList(WatchList $watchList): static
|
||||
public function addWatchlists(Watchlist $watchlist): static
|
||||
{
|
||||
if (!$this->watchLists->contains($watchList)) {
|
||||
$this->watchLists->add($watchList);
|
||||
$watchList->addDomain($this);
|
||||
if (!$this->watchlists->contains($watchlist)) {
|
||||
$this->watchlists->add($watchlist);
|
||||
$watchlist->addDomain($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeWatchList(WatchList $watchList): static
|
||||
public function removeWatchlists(Watchlist $watchlist): static
|
||||
{
|
||||
if ($this->watchLists->removeElement($watchList)) {
|
||||
$watchList->removeDomain($this);
|
||||
if ($this->watchlists->removeElement($watchlist)) {
|
||||
$watchlist->removeDomain($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
@ -333,7 +348,7 @@ class Domain
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function setUpdatedAt(?\DateTimeImmutable $updatedAt): void
|
||||
public function setUpdatedAt(?\DateTimeImmutable $updatedAt): void
|
||||
{
|
||||
$this->updatedAt = $updatedAt;
|
||||
}
|
||||
@ -378,7 +393,7 @@ class Domain
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function isToBeWatchClosely(): bool
|
||||
public function isToBeWatchClosely(): bool
|
||||
{
|
||||
$status = $this->getStatus();
|
||||
if ((!empty($status) && count(array_intersect($status, self::IMPORTANT_STATUS))) || $this->getDeleted()) {
|
||||
@ -395,47 +410,6 @@ class Domain
|
||||
return !empty($events) && in_array($events[0]->getAction(), self::IMPORTANT_EVENTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if one or more of these conditions are met:
|
||||
* - It has been more than 7 days since the domain name was last updated
|
||||
* - It has been more than 12 minutes and the domain name has statuses that suggest it is not stable
|
||||
* - It has been more than 1 day and the domain name is blocked in DNS
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function isToBeUpdated(bool $fromUser = true, bool $intensifyLastDay = false): bool
|
||||
{
|
||||
$updatedAtDiff = $this->getUpdatedAt()->diff(new \DateTimeImmutable());
|
||||
|
||||
if ($updatedAtDiff->days >= 7) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->getDeleted()) {
|
||||
return $fromUser;
|
||||
}
|
||||
|
||||
$expiresIn = $this->getExpiresInDays();
|
||||
|
||||
if ($intensifyLastDay && (0 === $expiresIn || 1 === $expiresIn)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$minutesDiff = $updatedAtDiff->h * 60 + $updatedAtDiff->i;
|
||||
if (($minutesDiff >= 12 || $fromUser) && $this->isToBeWatchClosely()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
count(array_intersect($this->getStatus(), ['auto renew period', 'client hold', 'server hold'])) > 0
|
||||
&& $updatedAtDiff->days >= 1
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, DomainStatus>
|
||||
*/
|
||||
@ -500,122 +474,6 @@ class Domain
|
||||
return in_array('pending delete', $this->getStatus()) && !in_array('redemption period', $this->getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DateMalformedIntervalStringException
|
||||
*/
|
||||
private function calculateDaysFromStatus(\DateTimeImmutable $now): ?int
|
||||
{
|
||||
$lastStatus = $this->getDomainStatuses()->last();
|
||||
if (false === $lastStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->isPendingDelete() && (
|
||||
in_array('pending delete', $lastStatus->getAddStatus())
|
||||
|| in_array('redemption period', $lastStatus->getDeleteStatus()))
|
||||
) {
|
||||
return self::daysBetween($now, $lastStatus->getCreatedAt()->add(new \DateInterval('P'. 5 .'D')));
|
||||
}
|
||||
|
||||
if ($this->isRedemptionPeriod()
|
||||
&& in_array('redemption period', $lastStatus->getAddStatus())
|
||||
) {
|
||||
return self::daysBetween($now, $lastStatus->getCreatedAt()->add(new \DateInterval('P'.(30 + 5).'D')));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
private function calculateDaysFromEvents(\DateTimeImmutable $now): ?int
|
||||
{
|
||||
$lastChangedEvent = $this->getEvents()->findFirst(fn (int $i, DomainEvent $e) => !$e->getDeleted() && EventAction::LastChanged->value === $e->getAction());
|
||||
if (null === $lastChangedEvent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->isRedemptionPeriod()) {
|
||||
return self::daysBetween($now, $lastChangedEvent->getDate()->add(new \DateInterval('P'.(30 + 5).'D')));
|
||||
}
|
||||
if ($this->isPendingDelete()) {
|
||||
return self::daysBetween($now, $lastChangedEvent->getDate()->add(new \DateInterval('P'. 5 .'D')));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
*/
|
||||
|
||||
private static function daysBetween(\DateTimeImmutable $start, \DateTimeImmutable $end): int
|
||||
{
|
||||
$interval = $start->setTime(0, 0)->diff($end->setTime(0, 0));
|
||||
|
||||
return $interval->invert ? -$interval->days : $interval->days;
|
||||
}
|
||||
|
||||
private static function returnExpiresIn(array $guesses): ?int
|
||||
{
|
||||
$filteredGuesses = array_filter($guesses, function ($value) {
|
||||
return null !== $value;
|
||||
});
|
||||
|
||||
if (empty($filteredGuesses)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return max(min($filteredGuesses), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function getRelevantDates(): array
|
||||
{
|
||||
$expiredAt = $deletedAt = null;
|
||||
foreach ($this->getEvents()->getIterator() as $event) {
|
||||
if (!$event->getDeleted()) {
|
||||
if ('expiration' === $event->getAction()) {
|
||||
$expiredAt = $event->getDate();
|
||||
} elseif ('deletion' === $event->getAction()) {
|
||||
$deletedAt = $event->getDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [$expiredAt, $deletedAt];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
#[Groups(['domain:item', 'domain:list'])]
|
||||
public function getExpiresInDays(): ?int
|
||||
{
|
||||
if ($this->getDeleted()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$now = new \DateTimeImmutable();
|
||||
[$expiredAt, $deletedAt] = $this->getRelevantDates();
|
||||
|
||||
if ($expiredAt) {
|
||||
$guess = self::daysBetween($now, $expiredAt->add(new \DateInterval('P'.(45 + 30 + 5).'D')));
|
||||
}
|
||||
|
||||
if ($deletedAt) {
|
||||
// It has been observed that AFNIC, on the last day, adds a "deleted" event and removes the redemption period status.
|
||||
if (0 === self::daysBetween($now, $deletedAt) && $this->isPendingDelete()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$guess = self::daysBetween($now, $deletedAt->add(new \DateInterval('P'. 30 .'D')));
|
||||
}
|
||||
|
||||
return self::returnExpiresIn([
|
||||
$guess ?? null,
|
||||
$this->calculateDaysFromStatus($now),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, DnsKey>
|
||||
*/
|
||||
@ -646,69 +504,16 @@ class Domain
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Event[]
|
||||
*
|
||||
* @throws ParseException
|
||||
* @throws EofException
|
||||
* @throws InvalidDataException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getDomainCalendarEvents(): array
|
||||
#[Groups(['domain:item', 'domain:list'])]
|
||||
public function getExpiresInDays(): ?int
|
||||
{
|
||||
$events = [];
|
||||
$attendees = [];
|
||||
return $this->expiresInDays;
|
||||
}
|
||||
|
||||
/* @var DomainEntity $entity */
|
||||
foreach ($this->getDomainEntities()->filter(fn (DomainEntity $domainEntity) => !$domainEntity->getDeleted())->getIterator() as $domainEntity) {
|
||||
$jCard = $domainEntity->getEntity()->getJCard();
|
||||
public function setExpiresInDays(?int $expiresInDays): static
|
||||
{
|
||||
$this->expiresInDays = $expiresInDays;
|
||||
|
||||
if (empty($jCard)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$vCardData = Reader::readJson($jCard);
|
||||
|
||||
if (empty($vCardData->EMAIL) || empty($vCardData->FN)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$email = (string) $vCardData->EMAIL;
|
||||
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attendees[] = (new Attendee(new EmailAddress($email)))->setDisplayName((string) $vCardData->FN);
|
||||
}
|
||||
|
||||
/** @var DomainEvent $event */
|
||||
foreach ($this->getEvents()->filter(fn (DomainEvent $e) => $e->getDate()->diff(new \DateTimeImmutable('now'))->y <= 10)->getIterator() as $event) {
|
||||
$events[] = (new Event())
|
||||
->setLastModified(new Timestamp($this->getUpdatedAt()))
|
||||
->setStatus(EventStatus::CONFIRMED())
|
||||
->setSummary($this->getLdhName().': '.$event->getAction())
|
||||
->addCategory(new Category($event->getAction()))
|
||||
->setAttendees($attendees)
|
||||
->setOccurrence(new SingleDay(new Date($event->getDate()))
|
||||
);
|
||||
}
|
||||
|
||||
$expiresInDays = $this->getExpiresInDays();
|
||||
|
||||
if (null !== $expiresInDays) {
|
||||
$events[] = (new Event())
|
||||
->setLastModified(new Timestamp($this->getUpdatedAt()))
|
||||
->setStatus(EventStatus::CONFIRMED())
|
||||
->setSummary($this->getLdhName().': estimated WHOIS release date')
|
||||
->addCategory(new Category('release'))
|
||||
->setAttendees($attendees)
|
||||
->setOccurrence(new SingleDay(new Date(
|
||||
(new \DateTimeImmutable())->setTime(0, 0)->add(new \DateInterval('P'.$expiresInDays.'D'))
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
return $events;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,18 +23,13 @@ class DomainEntity
|
||||
#[Groups(['domain-entity:entity'])]
|
||||
private ?Entity $entity = null;
|
||||
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY)]
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
#[Groups(['domain-entity:entity', 'domain-entity:domain'])]
|
||||
private array $roles = [];
|
||||
|
||||
#[ORM\Column]
|
||||
#[ORM\Column(nullable: true)]
|
||||
#[Groups(['domain-entity:entity', 'domain-entity:domain'])]
|
||||
private ?bool $deleted;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->deleted = false;
|
||||
}
|
||||
private ?\DateTimeImmutable $deletedAt = null;
|
||||
|
||||
public function getDomain(): ?Domain
|
||||
{
|
||||
@ -75,14 +70,14 @@ class DomainEntity
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDeleted(): ?bool
|
||||
public function getDeletedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->deleted;
|
||||
return $this->deletedAt;
|
||||
}
|
||||
|
||||
public function setDeleted(?bool $deleted): static
|
||||
public function setDeletedAt(?\DateTimeImmutable $deletedAt): static
|
||||
{
|
||||
$this->deleted = $deleted;
|
||||
$this->deletedAt = $deletedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -27,11 +27,11 @@ class DomainStatus
|
||||
#[Groups(['domain:item'])]
|
||||
private \DateTimeImmutable $date;
|
||||
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)]
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['domain:item'])]
|
||||
private array $addStatus = [];
|
||||
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)]
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['domain:item'])]
|
||||
private array $deleteStatus = [];
|
||||
|
||||
|
||||
@ -2,15 +2,12 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use App\Repository\EntityRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\ORM\Mapping\Embedded;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
|
||||
@ -20,31 +17,6 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
)]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(
|
||||
uriTemplate: '/entities/icann-accreditations',
|
||||
openapiContext: [
|
||||
'parameters' => [
|
||||
[
|
||||
'name' => 'icannAccreditation.status',
|
||||
'in' => 'query',
|
||||
'required' => true,
|
||||
'schema' => [
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'enum' => ['Accredited', 'Terminated', 'Reserved'],
|
||||
],
|
||||
],
|
||||
'style' => 'form',
|
||||
'explode' => true,
|
||||
'description' => 'Filter by ICANN accreditation status',
|
||||
],
|
||||
],
|
||||
],
|
||||
description: 'ICANN Registrar IDs list',
|
||||
normalizationContext: ['groups' => ['entity:list']],
|
||||
name: 'icann_accreditations_collection'
|
||||
),
|
||||
/*
|
||||
new GetCollection(
|
||||
uriTemplate: '/entities',
|
||||
@ -67,12 +39,6 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
*/
|
||||
]
|
||||
)]
|
||||
#[ApiFilter(
|
||||
SearchFilter::class,
|
||||
properties: [
|
||||
'icannAccreditation.status' => 'exact',
|
||||
]
|
||||
)]
|
||||
class Entity
|
||||
{
|
||||
#[ORM\Id]
|
||||
@ -81,12 +47,12 @@ class Entity
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Tld::class, inversedBy: 'entities')]
|
||||
#[ORM\JoinColumn(referencedColumnName: 'tld', nullable: true)]
|
||||
#[Groups(['entity:list', 'entity:item', 'domain:item'])]
|
||||
#[ORM\JoinColumn(referencedColumnName: 'tld', nullable: false)]
|
||||
#[Groups(['entity:list', 'entity:item', 'domain:item', 'watchlist:item'])]
|
||||
private ?Tld $tld = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Groups(['entity:list', 'entity:item', 'domain:item'])]
|
||||
#[Groups(['entity:list', 'entity:item', 'domain:item', 'watchlist:item'])]
|
||||
private ?string $handle = null;
|
||||
|
||||
/**
|
||||
@ -112,15 +78,21 @@ class Entity
|
||||
#[Groups(['entity:item', 'domain:item'])]
|
||||
private Collection $events;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['entity:item', 'domain:item'])]
|
||||
#[ORM\Column(type: 'json')]
|
||||
#[ApiProperty(
|
||||
openapiContext: [
|
||||
'type' => 'array',
|
||||
'items' => ['type' => 'array'],
|
||||
]
|
||||
)]
|
||||
#[Groups(['entity:item', 'domain:item', 'watchlist:item'])]
|
||||
private array $jCard = [];
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
#[Groups(['entity:item', 'domain:item'])]
|
||||
#[Groups(['entity:item', 'domain:item', 'watchlist:item'])]
|
||||
private ?array $remarks = null;
|
||||
|
||||
#[Embedded(class: IcannAccreditation::class, columnPrefix: 'icann_')]
|
||||
#[ORM\ManyToOne(inversedBy: 'entities')]
|
||||
#[Groups(['entity:list', 'entity:item', 'domain:item'])]
|
||||
private ?IcannAccreditation $icannAccreditation = null;
|
||||
|
||||
@ -129,7 +101,6 @@ class Entity
|
||||
$this->domainEntities = new ArrayCollection();
|
||||
$this->nameserverEntities = new ArrayCollection();
|
||||
$this->events = new ArrayCollection();
|
||||
$this->icannAccreditation = new IcannAccreditation();
|
||||
}
|
||||
|
||||
public function getHandle(): ?string
|
||||
@ -284,11 +255,13 @@ class Entity
|
||||
|
||||
public function getIcannAccreditation(): ?IcannAccreditation
|
||||
{
|
||||
return null === $this->icannAccreditation->getStatus() ? null : $this->icannAccreditation;
|
||||
return $this->icannAccreditation;
|
||||
}
|
||||
|
||||
public function setIcannAccreditation(?IcannAccreditation $icannAccreditation): void
|
||||
public function setIcannAccreditation(?IcannAccreditation $icannAccreditation): static
|
||||
{
|
||||
$this->icannAccreditation = $icannAccreditation;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,35 +2,92 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use App\Config\RegistrarStatus;
|
||||
use App\Repository\IcannAccreditationRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\ORM\Mapping\Embeddable;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[Embeddable]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(
|
||||
uriTemplate: '/icann-accreditations',
|
||||
openapiContext: [
|
||||
'parameters' => [
|
||||
[
|
||||
'name' => 'status',
|
||||
'in' => 'query',
|
||||
'required' => true,
|
||||
'schema' => [
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'enum' => ['Accredited', 'Terminated', 'Reserved'],
|
||||
],
|
||||
],
|
||||
'style' => 'form',
|
||||
'explode' => true,
|
||||
'description' => 'Filter by ICANN accreditation status',
|
||||
],
|
||||
],
|
||||
],
|
||||
shortName: 'ICANN Accreditation',
|
||||
description: 'ICANN Registrar IDs list',
|
||||
normalizationContext: ['groups' => ['icann:list']]
|
||||
),
|
||||
]
|
||||
)]
|
||||
#[ApiFilter(
|
||||
SearchFilter::class,
|
||||
properties: [
|
||||
'status' => 'exact',
|
||||
]
|
||||
)]
|
||||
#[ORM\Entity(repositoryClass: IcannAccreditationRepository::class)]
|
||||
class IcannAccreditation
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column]
|
||||
#[Groups(['icann:item', 'icann:list', 'domain:item'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['entity:item', 'entity:list', 'domain:item'])]
|
||||
#[Groups(['icann:item', 'icann:list', 'domain:item'])]
|
||||
private ?string $registrarName = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['entity:item', 'domain:item'])]
|
||||
#[Groups(['icann:item'])]
|
||||
private ?string $rdapBaseUrl = null;
|
||||
|
||||
#[ORM\Column(nullable: true, enumType: RegistrarStatus::class)]
|
||||
#[Groups(['entity:item', 'entity:list', 'domain:item'])]
|
||||
#[Groups(['icann:item', 'icann:list', 'domain:item'])]
|
||||
private ?RegistrarStatus $status = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||
#[Groups(['entity:item', 'entity:list', 'domain:item'])]
|
||||
#[Groups(['icann:item', 'icann:list', 'domain:item'])]
|
||||
private ?\DateTimeImmutable $updated = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||
#[Groups(['entity:item', 'entity:list', 'domain:item'])]
|
||||
#[Groups(['icann:item', 'icann:list', 'domain:item'])]
|
||||
private ?\DateTimeImmutable $date = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Entity>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Entity::class, mappedBy: 'icannAccreditation')]
|
||||
private Collection $entities;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->entities = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getRegistrarName(): ?string
|
||||
{
|
||||
return $this->registrarName;
|
||||
@ -90,4 +147,46 @@ class IcannAccreditation
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(?int $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Entity>
|
||||
*/
|
||||
public function getEntities(): Collection
|
||||
{
|
||||
return $this->entities;
|
||||
}
|
||||
|
||||
public function addEntity(Entity $entity): static
|
||||
{
|
||||
if (!$this->entities->contains($entity)) {
|
||||
$this->entities->add($entity);
|
||||
$entity->setIcannAccreditation($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeEntity(Entity $entity): static
|
||||
{
|
||||
if ($this->entities->removeElement($entity)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($entity->getIcannAccreditation() === $this) {
|
||||
$entity->setIcannAccreditation(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,7 +56,6 @@ class Nameserver
|
||||
* @var Collection<int, Domain>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Domain::class, mappedBy: 'nameservers')]
|
||||
#[Groups(['nameserver:item'])]
|
||||
private Collection $domains;
|
||||
|
||||
public function __construct()
|
||||
|
||||
@ -23,11 +23,11 @@ class NameserverEntity
|
||||
#[Groups(['nameserver-entity:entity'])]
|
||||
private ?Entity $entity = null;
|
||||
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY)]
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
#[Groups(['nameserver-entity:entity', 'nameserver-entity:nameserver'])]
|
||||
private array $roles = [];
|
||||
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY)]
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
#[Groups(['nameserver-entity:entity', 'nameserver-entity:nameserver'])]
|
||||
private array $status = [];
|
||||
|
||||
|
||||
@ -77,6 +77,9 @@ class Tld
|
||||
#[ORM\OneToMany(targetEntity: Entity::class, mappedBy: 'tld')]
|
||||
private Collection $entities;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||
private ?\DateTimeImmutable $deletedAt = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->rdapServers = new ArrayCollection();
|
||||
@ -241,4 +244,16 @@ class Tld
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDeletedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->deletedAt;
|
||||
}
|
||||
|
||||
public function setDeletedAt(?\DateTimeImmutable $deletedAt): static
|
||||
{
|
||||
$this->deletedAt = $deletedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,8 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
|
||||
@ -46,6 +48,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
|
||||
#[ORM\Column(length: 180)]
|
||||
#[Groups(['user:list', 'user:register'])]
|
||||
#[Assert\Email]
|
||||
#[Assert\NotBlank]
|
||||
private ?string $email = null;
|
||||
|
||||
/**
|
||||
@ -59,14 +63,13 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
* @var string|null The hashed password
|
||||
*/
|
||||
#[ORM\Column(nullable: true)]
|
||||
#[Groups(['user:register'])]
|
||||
private ?string $password = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, WatchList>
|
||||
* @var Collection<int, Watchlist>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: WatchList::class, mappedBy: 'user', orphanRemoval: true)]
|
||||
private Collection $watchLists;
|
||||
#[ORM\OneToMany(targetEntity: Watchlist::class, mappedBy: 'user', orphanRemoval: true)]
|
||||
private Collection $watchlists;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Connector>
|
||||
@ -75,11 +78,20 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
private Collection $connectors;
|
||||
|
||||
#[ORM\Column]
|
||||
private bool $isVerified = false;
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?\DateTimeImmutable $verifiedAt = null;
|
||||
|
||||
#[Assert\PasswordStrength]
|
||||
#[Assert\NotBlank]
|
||||
#[SerializedName('password')]
|
||||
#[Groups(['user:register'])]
|
||||
private ?string $plainPassword = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->watchLists = new ArrayCollection();
|
||||
$this->watchlists = new ArrayCollection();
|
||||
$this->connectors = new ArrayCollection();
|
||||
}
|
||||
|
||||
@ -152,33 +164,33 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
// If you store any temporary, sensitive data on the user, clear it here
|
||||
// $this->plainPassword = null;
|
||||
$this->plainPassword = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, WatchList>
|
||||
* @return Collection<int, Watchlist>
|
||||
*/
|
||||
public function getWatchLists(): Collection
|
||||
public function getWatchlists(): Collection
|
||||
{
|
||||
return $this->watchLists;
|
||||
return $this->watchlists;
|
||||
}
|
||||
|
||||
public function addWatchList(WatchList $watchList): static
|
||||
public function addWatchlist(Watchlist $watchlist): static
|
||||
{
|
||||
if (!$this->watchLists->contains($watchList)) {
|
||||
$this->watchLists->add($watchList);
|
||||
$watchList->setUser($this);
|
||||
if (!$this->watchlists->contains($watchlist)) {
|
||||
$this->watchlists->add($watchlist);
|
||||
$watchlist->setUser($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeWatchList(WatchList $watchList): static
|
||||
public function removeWatchlist(Watchlist $watchlist): static
|
||||
{
|
||||
if ($this->watchLists->removeElement($watchList)) {
|
||||
if ($this->watchlists->removeElement($watchlist)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($watchList->getUser() === $this) {
|
||||
$watchList->setUser(null);
|
||||
if ($watchlist->getUser() === $this) {
|
||||
$watchlist->setUser(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,14 +227,38 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isVerified(): bool
|
||||
public function getCreatedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->isVerified;
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setVerified(bool $isVerified): static
|
||||
public function setCreatedAt(\DateTimeImmutable $createdAt): static
|
||||
{
|
||||
$this->isVerified = $isVerified;
|
||||
$this->createdAt = $createdAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getVerifiedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->verifiedAt;
|
||||
}
|
||||
|
||||
public function setVerifiedAt(\DateTimeImmutable $verifiedAt): static
|
||||
{
|
||||
$this->verifiedAt = $verifiedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPlainPassword(): ?string
|
||||
{
|
||||
return $this->plainPassword;
|
||||
}
|
||||
|
||||
public function setPlainPassword(?string $plainPassword): self
|
||||
{
|
||||
$this->plainPassword = $plainPassword;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Link;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Config\TriggerAction;
|
||||
use App\Repository\EventTriggerRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: EventTriggerRepository::class)]
|
||||
#[ApiResource(
|
||||
uriTemplate: '/watchlists/{watchListId}/triggers/{action}/{event}',
|
||||
shortName: 'Watchlist Trigger',
|
||||
operations: [
|
||||
new Get(),
|
||||
new GetCollection(
|
||||
uriTemplate: '/watchlists/{watchListId}/triggers',
|
||||
uriVariables: [
|
||||
'watchListId' => new Link(fromProperty: 'token', toProperty: 'watchList', fromClass: WatchList::class),
|
||||
],
|
||||
),
|
||||
new Post(
|
||||
uriTemplate: '/watchlist-triggers',
|
||||
uriVariables: [],
|
||||
security: 'true'
|
||||
),
|
||||
new Delete(),
|
||||
],
|
||||
uriVariables: [
|
||||
'watchListId' => new Link(fromProperty: 'token', toProperty: 'watchList', fromClass: WatchList::class),
|
||||
'action' => 'action',
|
||||
'event' => 'event',
|
||||
],
|
||||
security: 'object.getWatchList().user == user',
|
||||
)]
|
||||
class WatchListTrigger
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(length: 255, nullable: false)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private ?string $event;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\ManyToOne(targetEntity: WatchList::class, inversedBy: 'watchListTriggers')]
|
||||
#[ORM\JoinColumn(referencedColumnName: 'token', nullable: false, onDelete: 'CASCADE')]
|
||||
private ?WatchList $watchList;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(nullable: false, enumType: TriggerAction::class)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private ?TriggerAction $action;
|
||||
|
||||
public function getEvent(): ?string
|
||||
{
|
||||
return $this->event;
|
||||
}
|
||||
|
||||
public function setEvent(string $event): static
|
||||
{
|
||||
$this->event = $event;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWatchList(): ?WatchList
|
||||
{
|
||||
return $this->watchList;
|
||||
}
|
||||
|
||||
public function setWatchList(?WatchList $watchList): static
|
||||
{
|
||||
$this->watchList = $watchList;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAction(): ?TriggerAction
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
public function setAction(TriggerAction $action): static
|
||||
{
|
||||
$this->action = $action;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@ -6,10 +6,11 @@ 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 ApiPlatform\Metadata\Put;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\State\WatchListUpdateProcessor;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use App\State\WatchlistUpdateProcessor;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
@ -17,8 +18,9 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: WatchListRepository::class)]
|
||||
#[ORM\Entity(repositoryClass: WatchlistRepository::class)]
|
||||
#[ApiResource(
|
||||
shortName: 'Watchlist',
|
||||
operations: [
|
||||
@ -41,7 +43,6 @@ use Symfony\Component\Uid\Uuid;
|
||||
'domain:list',
|
||||
'tld:list',
|
||||
'event:list',
|
||||
'domain:list',
|
||||
'event:list',
|
||||
],
|
||||
],
|
||||
@ -51,7 +52,7 @@ use Symfony\Component\Uid\Uuid;
|
||||
normalizationContext: [
|
||||
'groups' => [
|
||||
'watchlist:item',
|
||||
'domain:item',
|
||||
'domain:list',
|
||||
'event:list',
|
||||
'domain-entity:entity',
|
||||
'nameserver-entity:nameserver',
|
||||
@ -59,7 +60,7 @@ use Symfony\Component\Uid\Uuid;
|
||||
'tld:item',
|
||||
],
|
||||
],
|
||||
security: 'object.user == user'
|
||||
security: 'object.getUser() == user'
|
||||
),
|
||||
new Get(
|
||||
routeName: 'watchlist_calendar',
|
||||
@ -86,16 +87,22 @@ use Symfony\Component\Uid\Uuid;
|
||||
new Post(
|
||||
normalizationContext: ['groups' => 'watchlist:list'],
|
||||
denormalizationContext: ['groups' => 'watchlist:create'],
|
||||
processor: WatchListUpdateProcessor::class,
|
||||
processor: WatchlistUpdateProcessor::class,
|
||||
),
|
||||
new Put(
|
||||
normalizationContext: ['groups' => 'watchlist:item'],
|
||||
denormalizationContext: ['groups' => ['watchlist:create', 'watchlist:token']],
|
||||
security: 'object.user == user',
|
||||
processor: WatchListUpdateProcessor::class,
|
||||
normalizationContext: ['groups' => 'watchlist:list'],
|
||||
denormalizationContext: ['groups' => ['watchlist:update']],
|
||||
security: 'object.getUser() == user',
|
||||
processor: WatchlistUpdateProcessor::class,
|
||||
),
|
||||
new Patch(
|
||||
normalizationContext: ['groups' => 'watchlist:list'],
|
||||
denormalizationContext: ['groups' => ['watchlist:update']],
|
||||
security: 'object.getUser() == user',
|
||||
processor: WatchlistUpdateProcessor::class,
|
||||
),
|
||||
new Delete(
|
||||
security: 'object.user == user'
|
||||
security: 'object.getUser() == user'
|
||||
),
|
||||
new Get(
|
||||
routeName: 'watchlist_rss_status',
|
||||
@ -145,9 +152,9 @@ use Symfony\Component\Uid\Uuid;
|
||||
),
|
||||
],
|
||||
)]
|
||||
class WatchList
|
||||
class Watchlist
|
||||
{
|
||||
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'watchLists')]
|
||||
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'watchlists')]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
public ?User $user = null;
|
||||
|
||||
@ -159,27 +166,19 @@ class WatchList
|
||||
/**
|
||||
* @var Collection<int, Domain>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Domain::class, inversedBy: 'watchLists')]
|
||||
#[ORM\JoinTable(name: 'watch_lists_domains',
|
||||
joinColumns: [new ORM\JoinColumn(name: 'watch_list_token', referencedColumnName: 'token', onDelete: 'CASCADE')],
|
||||
#[ORM\ManyToMany(targetEntity: Domain::class, inversedBy: 'watchlists')]
|
||||
#[ORM\JoinTable(name: 'watchlist_domains',
|
||||
joinColumns: [new ORM\JoinColumn(name: 'watchlist_token', referencedColumnName: 'token', onDelete: 'CASCADE')],
|
||||
inverseJoinColumns: [new ORM\JoinColumn(name: 'domain_ldh_name', referencedColumnName: 'ldh_name', onDelete: 'CASCADE')])]
|
||||
#[Groups(['watchlist:create', 'watchlist:list', 'watchlist:item'])]
|
||||
#[Groups(['watchlist:create', 'watchlist:list', 'watchlist:item', 'watchlist:update'])]
|
||||
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'])]
|
||||
#[SerializedName('triggers')]
|
||||
private Collection $watchListTriggers;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'watchLists')]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
#[ORM\ManyToOne(inversedBy: 'watchlists')]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
private ?Connector $connector = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column]
|
||||
@ -187,15 +186,42 @@ class WatchList
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
|
||||
#[SerializedName('dsn')]
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Assert\Unique]
|
||||
#[Assert\All([
|
||||
new Assert\Type('string'),
|
||||
new Assert\NotBlank(),
|
||||
])]
|
||||
private ?array $webhookDsn = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Assert\Unique]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\All([
|
||||
new Assert\Type('string'),
|
||||
new Assert\NotBlank(),
|
||||
])]
|
||||
private array $trackedEvents = [];
|
||||
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Assert\Unique]
|
||||
#[Assert\All([
|
||||
new Assert\Type('string'),
|
||||
new Assert\NotBlank(),
|
||||
])]
|
||||
private array $trackedEppStatus = [];
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
private ?bool $enabled = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->token = Uuid::v4();
|
||||
$this->domains = new ArrayCollection();
|
||||
$this->watchListTriggers = new ArrayCollection();
|
||||
$this->createdAt = new \DateTimeImmutable('now');
|
||||
}
|
||||
|
||||
@ -245,36 +271,6 @@ class WatchList
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, WatchListTrigger>
|
||||
*/
|
||||
public function getWatchListTriggers(): Collection
|
||||
{
|
||||
return $this->watchListTriggers;
|
||||
}
|
||||
|
||||
public function addWatchListTrigger(WatchListTrigger $watchListTrigger): static
|
||||
{
|
||||
if (!$this->watchListTriggers->contains($watchListTrigger)) {
|
||||
$this->watchListTriggers->add($watchListTrigger);
|
||||
$watchListTrigger->setWatchList($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeWatchListTrigger(WatchListTrigger $watchListTrigger): static
|
||||
{
|
||||
if ($this->watchListTriggers->removeElement($watchListTrigger)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($watchListTrigger->getWatchList() === $this) {
|
||||
$watchListTrigger->setWatchList(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getConnector(): ?Connector
|
||||
{
|
||||
return $this->connector;
|
||||
@ -322,4 +318,40 @@ class WatchList
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTrackedEvents(): array
|
||||
{
|
||||
return $this->trackedEvents;
|
||||
}
|
||||
|
||||
public function setTrackedEvents(array $trackedEvents): static
|
||||
{
|
||||
$this->trackedEvents = $trackedEvents;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTrackedEppStatus(): array
|
||||
{
|
||||
return $this->trackedEppStatus;
|
||||
}
|
||||
|
||||
public function setTrackedEppStatus(array $trackedEppStatus): static
|
||||
{
|
||||
$this->trackedEppStatus = $trackedEppStatus;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isEnabled(): ?bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function setEnabled(bool $enabled): static
|
||||
{
|
||||
$this->enabled = $enabled;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
11
src/Exception/DomainNotFoundException.php
Normal file
11
src/Exception/DomainNotFoundException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
class DomainNotFoundException extends \Exception
|
||||
{
|
||||
public static function fromDomain(string $ldhName): self
|
||||
{
|
||||
return new self("The domain name $ldhName is not present in the WHOIS database");
|
||||
}
|
||||
}
|
||||
11
src/Exception/MalformedDomainException.php
Normal file
11
src/Exception/MalformedDomainException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
class MalformedDomainException extends \Exception
|
||||
{
|
||||
public static function fromDomain(string $ldhName): self
|
||||
{
|
||||
return new self("Malformed domain name ($ldhName)");
|
||||
}
|
||||
}
|
||||
7
src/Exception/Provider/AbstractProviderException.php
Normal file
7
src/Exception/Provider/AbstractProviderException.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
abstract class AbstractProviderException extends \Exception
|
||||
{
|
||||
}
|
||||
7
src/Exception/Provider/DomainOrderFailedExeption.php
Normal file
7
src/Exception/Provider/DomainOrderFailedExeption.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class DomainOrderFailedExeption extends AbstractProviderException
|
||||
{
|
||||
}
|
||||
11
src/Exception/Provider/EppContactIsAvailableException.php
Normal file
11
src/Exception/Provider/EppContactIsAvailableException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class EppContactIsAvailableException extends AbstractProviderException
|
||||
{
|
||||
public static function fromContact(string $handle): self
|
||||
{
|
||||
return new self("At least one of the entered contacts cannot be used because it is indicated as available ($handle)");
|
||||
}
|
||||
}
|
||||
11
src/Exception/Provider/ExpiredLoginException.php
Normal file
11
src/Exception/Provider/ExpiredLoginException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class ExpiredLoginException extends AbstractProviderException
|
||||
{
|
||||
public static function fromIdentifier(string $identifier): self
|
||||
{
|
||||
return new self("Expired login for identifier $identifier");
|
||||
}
|
||||
}
|
||||
16
src/Exception/Provider/InvalidLoginException.php
Normal file
16
src/Exception/Provider/InvalidLoginException.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class InvalidLoginException extends AbstractProviderException
|
||||
{
|
||||
public function __construct(string $message = '')
|
||||
{
|
||||
parent::__construct('' === $message ? 'The status of these credentials is not valid' : $message);
|
||||
}
|
||||
|
||||
public static function fromIdentifier(string $identifier): self
|
||||
{
|
||||
return new self("Invalid login for identifier $identifier");
|
||||
}
|
||||
}
|
||||
11
src/Exception/Provider/InvalidLoginStatusException.php
Normal file
11
src/Exception/Provider/InvalidLoginStatusException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class InvalidLoginStatusException extends AbstractProviderException
|
||||
{
|
||||
public static function fromStatus(string $status): self
|
||||
{
|
||||
return new self("The status of these credentials is not valid ($status)");
|
||||
}
|
||||
}
|
||||
11
src/Exception/Provider/NamecheapRequiresAddressException.php
Normal file
11
src/Exception/Provider/NamecheapRequiresAddressException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class NamecheapRequiresAddressException extends AbstractProviderException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('Namecheap account requires at least one address to purchase a domain');
|
||||
}
|
||||
}
|
||||
11
src/Exception/Provider/PermissionErrorException.php
Normal file
11
src/Exception/Provider/PermissionErrorException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class PermissionErrorException extends AbstractProviderException
|
||||
{
|
||||
public static function fromIdentifier(string $identifier): self
|
||||
{
|
||||
return new self("Not enough permissions for identifier $identifier");
|
||||
}
|
||||
}
|
||||
11
src/Exception/Provider/ProviderGenericErrorException.php
Normal file
11
src/Exception/Provider/ProviderGenericErrorException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class ProviderGenericErrorException extends AbstractProviderException
|
||||
{
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
}
|
||||
11
src/Exception/Provider/UserNoExplicitConsentException.php
Normal file
11
src/Exception/Provider/UserNoExplicitConsentException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class UserNoExplicitConsentException extends AbstractProviderException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('The user has not given explicit consent');
|
||||
}
|
||||
}
|
||||
11
src/Exception/TldNotSupportedException.php
Normal file
11
src/Exception/TldNotSupportedException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
class TldNotSupportedException extends \Exception
|
||||
{
|
||||
public static function fromTld(string $tld): self
|
||||
{
|
||||
return new self("The requested TLD $tld is not yet supported, please try again with another one");
|
||||
}
|
||||
}
|
||||
11
src/Exception/UnknownRdapServerException.php
Normal file
11
src/Exception/UnknownRdapServerException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
class UnknownRdapServerException extends \Exception
|
||||
{
|
||||
public static function fromTld(string $tld): self
|
||||
{
|
||||
return new self("TLD $tld: Unable to determine which RDAP server to contact");
|
||||
}
|
||||
}
|
||||
11
src/Exception/UnsupportedDsnSchemeException.php
Normal file
11
src/Exception/UnsupportedDsnSchemeException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
class UnsupportedDsnSchemeException extends \Exception
|
||||
{
|
||||
public static function fromScheme(string $scheme): UnsupportedDsnSchemeException
|
||||
{
|
||||
return new UnsupportedDsnSchemeException("The DSN scheme ($scheme) is not supported");
|
||||
}
|
||||
}
|
||||
64
src/Factory/UserFactory.php
Normal file
64
src/Factory/UserFactory.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Factory;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
|
||||
|
||||
/**
|
||||
* @extends PersistentObjectFactory<User>
|
||||
*/
|
||||
final class UserFactory extends PersistentObjectFactory
|
||||
{
|
||||
/**
|
||||
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
|
||||
*
|
||||
* @todo inject services if required
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public static function class(): string
|
||||
{
|
||||
return User::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
|
||||
*
|
||||
* @todo add your default values here
|
||||
*/
|
||||
#[\Override]
|
||||
protected function defaults(): array|callable
|
||||
{
|
||||
$createdAt = \DateTimeImmutable::createFromMutable(self::faker()->dateTime());
|
||||
$plainPassword = self::faker()->password(16, 20);
|
||||
|
||||
return [
|
||||
'createdAt' => $createdAt,
|
||||
'verifiedAt' => $createdAt,
|
||||
'email' => self::faker()->unique()->safeEmail(),
|
||||
'plainPassword' => $plainPassword,
|
||||
'roles' => ['ROLE_USER'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
|
||||
*/
|
||||
#[\Override]
|
||||
protected function initialize(): static
|
||||
{
|
||||
return $this->afterInstantiate(function (User $user): void {
|
||||
if ($user->getPlainPassword()) {
|
||||
$user->setPassword(
|
||||
$this->passwordHasher->hashPassword($user, $user->getPlainPassword())
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ namespace App\Message;
|
||||
final class OrderDomain
|
||||
{
|
||||
public function __construct(
|
||||
public string $watchListToken,
|
||||
public string $watchlistToken,
|
||||
public string $ldhName,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Message;
|
||||
|
||||
final class ProcessWatchListsTrigger
|
||||
final class ProcessWatchlistTrigger
|
||||
{
|
||||
/*
|
||||
* Add whatever properties and methods you need
|
||||
@ -5,7 +5,7 @@ namespace App\Message;
|
||||
final class SendDomainEventNotif
|
||||
{
|
||||
public function __construct(
|
||||
public string $watchListToken,
|
||||
public string $watchlistToken,
|
||||
public string $ldhName,
|
||||
public \DateTimeImmutable $updatedAt,
|
||||
) {
|
||||
|
||||
@ -5,7 +5,7 @@ namespace App\Message;
|
||||
final readonly class UpdateDomainsFromWatchlist
|
||||
{
|
||||
public function __construct(
|
||||
public string $watchListToken,
|
||||
public string $watchlistToken,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,15 +3,15 @@
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\WatchList;
|
||||
use App\Entity\Watchlist;
|
||||
use App\Message\OrderDomain;
|
||||
use App\Notifier\DomainOrderErrorNotification;
|
||||
use App\Notifier\DomainOrderNotification;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use App\Service\ChatNotificationService;
|
||||
use App\Service\Connector\AbstractProvider;
|
||||
use App\Service\InfluxdbService;
|
||||
use App\Service\Provider\AbstractProvider;
|
||||
use App\Service\StatService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
@ -31,7 +31,7 @@ final readonly class OrderDomainHandler
|
||||
public function __construct(
|
||||
string $mailerSenderEmail,
|
||||
string $mailerSenderName,
|
||||
private WatchListRepository $watchListRepository,
|
||||
private WatchlistRepository $watchlistRepository,
|
||||
private DomainRepository $domainRepository,
|
||||
private KernelInterface $kernel,
|
||||
private MailerInterface $mailer,
|
||||
@ -54,12 +54,12 @@ final readonly class OrderDomainHandler
|
||||
*/
|
||||
public function __invoke(OrderDomain $message): void
|
||||
{
|
||||
/** @var WatchList $watchList */
|
||||
$watchList = $this->watchListRepository->findOneBy(['token' => $message->watchListToken]);
|
||||
/** @var Watchlist $watchlist */
|
||||
$watchlist = $this->watchlistRepository->findOneBy(['token' => $message->watchlistToken]);
|
||||
/** @var Domain $domain */
|
||||
$domain = $this->domainRepository->findOneBy(['ldhName' => $message->ldhName]);
|
||||
|
||||
$connector = $watchList->getConnector();
|
||||
$connector = $watchlist->getConnector();
|
||||
|
||||
/*
|
||||
* We make sure that the domain name is marked absent from WHOIS in the database before continuing.
|
||||
@ -71,8 +71,8 @@ final readonly class OrderDomainHandler
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->notice('Watchlist {watchlist} is linked to connector {connector}. A purchase attempt will be made for domain name {ldhName} with provider {provider}.', [
|
||||
'watchlist' => $message->watchListToken,
|
||||
$this->logger->notice('Watchlist is linked to a connector : a purchase attempt will be made for this domain name', [
|
||||
'watchlist' => $message->watchlistToken,
|
||||
'connector' => $connector->getId(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'provider' => $connector->getProvider()->value,
|
||||
@ -93,14 +93,15 @@ final readonly class OrderDomainHandler
|
||||
* The user is authenticated to ensure that the credentials are still valid.
|
||||
* If no errors occur, the purchase is attempted.
|
||||
*/
|
||||
$connectorProvider->authenticate($connector->getAuthData());
|
||||
|
||||
$connectorProvider->orderDomain($domain, $this->kernel->isDebug());
|
||||
|
||||
/*
|
||||
* If the purchase was successful, the statistics are updated and a success message is sent to the user.
|
||||
*/
|
||||
$this->logger->notice('Watchlist {watchlist} is linked to connector {connector}. A purchase was successfully made for domain {ldhName} with provider {provider}.', [
|
||||
'watchlist' => $message->watchListToken,
|
||||
$this->logger->notice('Watchlist is linked to connector : a purchase was successfully made for this domain name', [
|
||||
'watchlist' => $message->watchlistToken,
|
||||
'connector' => $connector->getId(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'provider' => $connector->getProvider()->value,
|
||||
@ -111,15 +112,18 @@ final readonly class OrderDomainHandler
|
||||
$this->influxdbService->addDomainOrderPoint($connector, $domain, true);
|
||||
}
|
||||
$notification = (new DomainOrderNotification($this->sender, $domain, $connector));
|
||||
$this->mailer->send($notification->asEmailMessage(new Recipient($watchList->getUser()->getEmail()))->getMessage());
|
||||
$this->chatNotificationService->sendChatNotification($watchList, $notification);
|
||||
$this->mailer->send($notification->asEmailMessage(new Recipient($watchlist->getUser()->getEmail()))->getMessage());
|
||||
$this->chatNotificationService->sendChatNotification($watchlist, $notification);
|
||||
} catch (\Throwable $exception) {
|
||||
/*
|
||||
* The purchase was not successful (for several possible reasons that we have not determined).
|
||||
* The user is informed and the exception is raised, which may allow you to try again.
|
||||
*/
|
||||
$this->logger->warning('Unable to complete purchase. An error message is sent to user {username}.', [
|
||||
'username' => $watchList->getUser()->getUserIdentifier(),
|
||||
$this->logger->warning('Unable to complete purchase : an error message is sent to the user', [
|
||||
'watchlist' => $message->watchlistToken,
|
||||
'connector' => $connector->getId(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'provider' => $connector->getProvider()->value,
|
||||
]);
|
||||
|
||||
$this->statService->incrementStat('stats.domain.purchase.failed');
|
||||
@ -127,8 +131,8 @@ final readonly class OrderDomainHandler
|
||||
$this->influxdbService->addDomainOrderPoint($connector, $domain, false);
|
||||
}
|
||||
$notification = (new DomainOrderErrorNotification($this->sender, $domain));
|
||||
$this->mailer->send($notification->asEmailMessage(new Recipient($watchList->getUser()->getEmail()))->getMessage());
|
||||
$this->chatNotificationService->sendChatNotification($watchList, $notification);
|
||||
$this->mailer->send($notification->asEmailMessage(new Recipient($watchlist->getUser()->getEmail()))->getMessage());
|
||||
$this->chatNotificationService->sendChatNotification($watchlist, $notification);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
@ -2,20 +2,20 @@
|
||||
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Entity\WatchList;
|
||||
use App\Message\ProcessWatchListsTrigger;
|
||||
use App\Entity\Watchlist;
|
||||
use App\Message\ProcessWatchlistTrigger;
|
||||
use App\Message\UpdateDomainsFromWatchlist;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use Random\Randomizer;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Messenger\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
#[AsMessageHandler]
|
||||
final readonly class ProcessWatchListsTriggerHandler
|
||||
final readonly class ProcessWatchlistTriggerHandler
|
||||
{
|
||||
public function __construct(
|
||||
private WatchListRepository $watchListRepository,
|
||||
private WatchlistRepository $watchlistRepository,
|
||||
private MessageBusInterface $bus,
|
||||
) {
|
||||
}
|
||||
@ -23,7 +23,7 @@ final readonly class ProcessWatchListsTriggerHandler
|
||||
/**
|
||||
* @throws ExceptionInterface
|
||||
*/
|
||||
public function __invoke(ProcessWatchListsTrigger $message): void
|
||||
public function __invoke(ProcessWatchlistTrigger $message): void
|
||||
{
|
||||
/*
|
||||
* We shuffle the watch lists to process them in an order that we consider random.
|
||||
@ -31,11 +31,11 @@ final readonly class ProcessWatchListsTriggerHandler
|
||||
*/
|
||||
|
||||
$randomizer = new Randomizer();
|
||||
$watchLists = $randomizer->shuffleArray($this->watchListRepository->findAll());
|
||||
$watchlists = $randomizer->shuffleArray($this->watchlistRepository->getEnabledWatchlist());
|
||||
|
||||
/** @var WatchList $watchList */
|
||||
foreach ($watchLists as $watchList) {
|
||||
$this->bus->dispatch(new UpdateDomainsFromWatchlist($watchList->getToken()));
|
||||
/** @var Watchlist $watchlist */
|
||||
foreach ($watchlists as $watchlist) {
|
||||
$this->bus->dispatch(new UpdateDomainsFromWatchlist($watchlist->getToken()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,15 +2,17 @@
|
||||
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Config\TriggerAction;
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\DomainEvent;
|
||||
use App\Entity\WatchList;
|
||||
use App\Entity\WatchListTrigger;
|
||||
use App\Entity\DomainStatus;
|
||||
use App\Entity\Watchlist;
|
||||
use App\Message\SendDomainEventNotif;
|
||||
use App\Notifier\DomainStatusUpdateNotification;
|
||||
use App\Notifier\DomainUpdateNotification;
|
||||
use App\Repository\DomainEventRepository;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Repository\DomainStatusRepository;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use App\Service\ChatNotificationService;
|
||||
use App\Service\InfluxdbService;
|
||||
use App\Service\StatService;
|
||||
@ -19,7 +21,6 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Messenger\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Notifier\Recipient\Recipient;
|
||||
|
||||
@ -35,11 +36,13 @@ final readonly class SendDomainEventNotifHandler
|
||||
private MailerInterface $mailer,
|
||||
private StatService $statService,
|
||||
private DomainRepository $domainRepository,
|
||||
private WatchListRepository $watchListRepository,
|
||||
private WatchlistRepository $watchlistRepository,
|
||||
private ChatNotificationService $chatNotificationService,
|
||||
#[Autowire(param: 'influxdb_enabled')]
|
||||
private bool $influxdbEnabled,
|
||||
private InfluxdbService $influxdbService,
|
||||
private DomainEventRepository $domainEventRepository,
|
||||
private DomainStatusRepository $domainStatusRepository,
|
||||
) {
|
||||
$this->sender = new Address($mailerSenderEmail, $mailerSenderName);
|
||||
}
|
||||
@ -47,56 +50,99 @@ final readonly class SendDomainEventNotifHandler
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws \Exception
|
||||
* @throws ExceptionInterface
|
||||
*/
|
||||
public function __invoke(SendDomainEventNotif $message): void
|
||||
{
|
||||
/** @var WatchList $watchList */
|
||||
$watchList = $this->watchListRepository->findOneBy(['token' => $message->watchListToken]);
|
||||
/** @var Watchlist $watchlist */
|
||||
$watchlist = $this->watchlistRepository->findOneBy(['token' => $message->watchlistToken]);
|
||||
/** @var Domain $domain */
|
||||
$domain = $this->domainRepository->findOneBy(['ldhName' => $message->ldhName]);
|
||||
$recipient = new Recipient($watchlist->getUser()->getEmail());
|
||||
|
||||
/*
|
||||
* For each new event whose date is after the domain name update date (before the current domain name update)
|
||||
*/
|
||||
|
||||
/** @var DomainEvent $event */
|
||||
foreach ($domain->getEvents()->filter(
|
||||
fn ($event) => $message->updatedAt < $event->getDate() && $event->getDate() < new \DateTimeImmutable()) as $event
|
||||
) {
|
||||
$watchListTriggers = $watchList->getWatchListTriggers()
|
||||
->filter(fn ($trigger) => $trigger->getEvent() === $event->getAction());
|
||||
/** @var DomainEvent[] $newEvents */
|
||||
$newEvents = $this->domainEventRepository->findNewDomainEvents($domain, $message->updatedAt);
|
||||
|
||||
/*
|
||||
* For each trigger, we perform the appropriate action: send email or send push notification (for now)
|
||||
*/
|
||||
foreach ($newEvents as $event) {
|
||||
if (!in_array($event->getAction(), $watchlist->getTrackedEvents())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var WatchListTrigger $watchListTrigger */
|
||||
foreach ($watchListTriggers->getIterator() as $watchListTrigger) {
|
||||
$this->logger->info('Action {event} has been detected on the domain name {ldhName}. A notification is sent to user {username}.', [
|
||||
$notification = new DomainUpdateNotification($this->sender, $event);
|
||||
|
||||
$this->logger->info('New action has been detected on this domain name : an email is sent to user', [
|
||||
'event' => $event->getAction(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'username' => $watchlist->getUser()->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
$this->mailer->send($notification->asEmailMessage($recipient)->getMessage());
|
||||
|
||||
if ($this->influxdbEnabled) {
|
||||
$this->influxdbService->addDomainNotificationPoint($domain, 'email', true);
|
||||
}
|
||||
|
||||
$webhookDsn = $watchlist->getWebhookDsn();
|
||||
if (null !== $webhookDsn && 0 !== count($webhookDsn)) {
|
||||
$this->logger->info('New action has been detected on this domain name : a notification is sent to user', [
|
||||
'event' => $event->getAction(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'username' => $watchList->getUser()->getUserIdentifier(),
|
||||
'username' => $watchlist->getUser()->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
$recipient = new Recipient($watchList->getUser()->getEmail());
|
||||
$notification = new DomainUpdateNotification($this->sender, $event);
|
||||
|
||||
if (TriggerAction::SendEmail == $watchListTrigger->getAction()) {
|
||||
$this->mailer->send($notification->asEmailMessage($recipient)->getMessage());
|
||||
} elseif (TriggerAction::SendChat == $watchListTrigger->getAction()) {
|
||||
$webhookDsn = $watchList->getWebhookDsn();
|
||||
if (null === $webhookDsn || 0 === count($webhookDsn)) {
|
||||
continue;
|
||||
}
|
||||
$this->chatNotificationService->sendChatNotification($watchList, $notification);
|
||||
$this->chatNotificationService->sendChatNotification($watchlist, $notification);
|
||||
if ($this->influxdbEnabled) {
|
||||
$this->influxdbService->addDomainNotificationPoint($domain, 'chat', true);
|
||||
}
|
||||
}
|
||||
|
||||
$this->statService->incrementStat('stats.alert.sent');
|
||||
}
|
||||
|
||||
/** @var DomainStatus $domainStatus */
|
||||
$domainStatus = $this->domainStatusRepository->findNewDomainStatus($domain, $message->updatedAt);
|
||||
|
||||
if (null !== $domainStatus && count(array_intersect(
|
||||
$watchlist->getTrackedEppStatus(),
|
||||
[...$domainStatus->getAddStatus(), ...$domainStatus->getDeleteStatus()]
|
||||
))) {
|
||||
$notification = new DomainStatusUpdateNotification($this->sender, $domain, $domainStatus);
|
||||
|
||||
$this->logger->info('New domain status has been detected on this domain name : an email is sent to user', [
|
||||
'addStatus' => $domainStatus->getAddStatus(),
|
||||
'deleteStatus' => $domainStatus->getDeleteStatus(),
|
||||
'status' => $domain->getStatus(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'username' => $watchlist->getUser()->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
$this->mailer->send($notification->asEmailMessage($recipient)->getMessage());
|
||||
|
||||
if ($this->influxdbEnabled) {
|
||||
$this->influxdbService->addDomainNotificationPoint($domain, 'email', true);
|
||||
}
|
||||
|
||||
$webhookDsn = $watchlist->getWebhookDsn();
|
||||
if (null !== $webhookDsn && 0 !== count($webhookDsn)) {
|
||||
$this->logger->info('New domain status has been detected on this domain name : a notification is sent to user', [
|
||||
'addStatus' => $domainStatus->getAddStatus(),
|
||||
'deleteStatus' => $domainStatus->getDeleteStatus(),
|
||||
'status' => $domain->getStatus(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'username' => $watchlist->getUser()->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
$this->chatNotificationService->sendChatNotification($watchlist, $notification);
|
||||
|
||||
if ($this->influxdbEnabled) {
|
||||
$this->influxdbService->addDomainNotificationPoint($domain, TriggerAction::SendChat, true);
|
||||
$this->influxdbService->addDomainNotificationPoint($domain, 'chat', true);
|
||||
}
|
||||
$this->statService->incrementStat('stats.alert.sent');
|
||||
}
|
||||
|
||||
$this->statService->incrementStat('stats.alert.sent');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,21 +3,23 @@
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\WatchList;
|
||||
use App\Entity\Watchlist;
|
||||
use App\Exception\DomainNotFoundException;
|
||||
use App\Exception\TldNotSupportedException;
|
||||
use App\Exception\UnknownRdapServerException;
|
||||
use App\Message\OrderDomain;
|
||||
use App\Message\SendDomainEventNotif;
|
||||
use App\Message\UpdateDomainsFromWatchlist;
|
||||
use App\Notifier\DomainDeletedNotification;
|
||||
use App\Notifier\DomainUpdateErrorNotification;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use App\Service\ChatNotificationService;
|
||||
use App\Service\Connector\AbstractProvider;
|
||||
use App\Service\Connector\CheckDomainProviderInterface;
|
||||
use App\Service\Provider\AbstractProvider;
|
||||
use App\Service\Provider\CheckDomainProviderInterface;
|
||||
use App\Service\RDAPService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
@ -36,10 +38,11 @@ final readonly class UpdateDomainsFromWatchlistHandler
|
||||
string $mailerSenderEmail,
|
||||
string $mailerSenderName,
|
||||
private MessageBusInterface $bus,
|
||||
private WatchListRepository $watchListRepository,
|
||||
private WatchlistRepository $watchlistRepository,
|
||||
private LoggerInterface $logger,
|
||||
#[Autowire(service: 'service_container')]
|
||||
private ContainerInterface $locator,
|
||||
private DomainRepository $domainRepository,
|
||||
) {
|
||||
$this->sender = new Address($mailerSenderEmail, $mailerSenderName);
|
||||
}
|
||||
@ -49,36 +52,37 @@ final readonly class UpdateDomainsFromWatchlistHandler
|
||||
*/
|
||||
public function __invoke(UpdateDomainsFromWatchlist $message): void
|
||||
{
|
||||
/** @var WatchList $watchList */
|
||||
$watchList = $this->watchListRepository->findOneBy(['token' => $message->watchListToken]);
|
||||
/** @var Watchlist $watchlist */
|
||||
$watchlist = $this->watchlistRepository->findOneBy(['token' => $message->watchlistToken]);
|
||||
|
||||
$this->logger->info('Domain names from Watchlist {token} will be processed.', [
|
||||
'token' => $message->watchListToken,
|
||||
$this->logger->debug('Domain names listed in the Watchlist will be updated', [
|
||||
'watchlist' => $message->watchlistToken,
|
||||
]);
|
||||
|
||||
/** @var AbstractProvider $connectorProvider */
|
||||
$connectorProvider = $this->getConnectorProvider($watchList);
|
||||
$connectorProvider = $this->getConnectorProvider($watchlist);
|
||||
|
||||
if ($connectorProvider instanceof CheckDomainProviderInterface) {
|
||||
$this->logger->notice('Watchlist {watchlist} linked to connector {connector}.', [
|
||||
'watchlist' => $watchList->getToken(),
|
||||
'connector' => $watchList->getConnector()->getId(),
|
||||
$this->logger->debug('Watchlist is linked to a connector', [
|
||||
'watchlist' => $watchlist->getToken(),
|
||||
'connector' => $watchlist->getConnector()->getId(),
|
||||
]);
|
||||
|
||||
$domainList = array_unique(array_map(fn (Domain $d) => $d->getLdhName(), $watchlist->getDomains()->toArray()));
|
||||
|
||||
try {
|
||||
$checkedDomains = $connectorProvider->checkDomains(
|
||||
...array_unique(array_map(fn (Domain $d) => $d->getLdhName(), $watchList->getDomains()->toArray()))
|
||||
);
|
||||
$checkedDomains = $connectorProvider->checkDomains(...$domainList);
|
||||
} catch (\Throwable $exception) {
|
||||
$this->logger->warning('Unable to check domain names availability with connector {connector}.', [
|
||||
'connector' => $watchList->getConnector()->getId(),
|
||||
$this->logger->warning('Unable to check domain names availability with this connector', [
|
||||
'connector' => $watchlist->getConnector()->getId(),
|
||||
'ldhName' => $domainList,
|
||||
]);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
foreach ($checkedDomains as $domain) {
|
||||
$this->bus->dispatch(new OrderDomain($watchList->getToken(), $domain));
|
||||
$this->bus->dispatch(new OrderDomain($watchlist->getToken(), $domain));
|
||||
}
|
||||
|
||||
return;
|
||||
@ -92,9 +96,10 @@ final readonly class UpdateDomainsFromWatchlistHandler
|
||||
*/
|
||||
|
||||
/** @var Domain $domain */
|
||||
foreach ($watchList->getDomains()->filter(fn ($domain) => $domain->isToBeUpdated(false, null !== $watchList->getConnector())) as $domain
|
||||
foreach ($watchlist->getDomains()->filter(fn ($domain) => $this->RDAPService->isToBeUpdated($domain, false, null !== $watchlist->getConnector())) as $domain
|
||||
) {
|
||||
$updatedAt = $domain->getUpdatedAt();
|
||||
$deleted = $domain->getDeleted();
|
||||
|
||||
try {
|
||||
/*
|
||||
@ -102,42 +107,34 @@ final readonly class UpdateDomainsFromWatchlistHandler
|
||||
* We send messages that correspond to the sending of notifications that will not be processed here.
|
||||
*/
|
||||
$this->RDAPService->registerDomain($domain->getLdhName());
|
||||
$this->bus->dispatch(new SendDomainEventNotif($watchList->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
} catch (NotFoundHttpException) {
|
||||
if (!$domain->getDeleted()) {
|
||||
$this->bus->dispatch(new SendDomainEventNotif($watchlist->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
} catch (DomainNotFoundException) {
|
||||
$newDomain = $this->domainRepository->findOneBy(['ldhName' => $domain->getLdhName()]);
|
||||
|
||||
if (!$deleted && null !== $newDomain && $newDomain->getDeleted()) {
|
||||
$notification = new DomainDeletedNotification($this->sender, $domain);
|
||||
$this->mailer->send($notification->asEmailMessage(new Recipient($watchList->getUser()->getEmail()))->getMessage());
|
||||
$this->chatNotificationService->sendChatNotification($watchList, $notification);
|
||||
$this->mailer->send($notification->asEmailMessage(new Recipient($watchlist->getUser()->getEmail()))->getMessage());
|
||||
$this->chatNotificationService->sendChatNotification($watchlist, $notification);
|
||||
}
|
||||
|
||||
if ($watchList->getConnector()) {
|
||||
if ($watchlist->getConnector()) {
|
||||
/*
|
||||
* If the domain name no longer appears in the WHOIS AND a connector is associated with this Watchlist,
|
||||
* this connector is used to purchase the domain name.
|
||||
*/
|
||||
$this->bus->dispatch(new OrderDomain($watchList->getToken(), $domain->getLdhName()));
|
||||
$this->bus->dispatch(new OrderDomain($watchlist->getToken(), $domain->getLdhName()));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
} catch (TldNotSupportedException|UnknownRdapServerException) {
|
||||
/*
|
||||
* In case of another unknown error,
|
||||
* the owner of the Watchlist is informed that an error occurred in updating the domain name.
|
||||
* In this case, the domain name can no longer be updated. Unfortunately, there is nothing more that can be done.
|
||||
*/
|
||||
$this->logger->error('An update error email is sent to user {username}.', [
|
||||
'username' => $watchList->getUser()->getUserIdentifier(),
|
||||
'error' => $e,
|
||||
]);
|
||||
$email = (new DomainUpdateErrorNotification($this->sender, $domain))
|
||||
->asEmailMessage(new Recipient($watchList->getUser()->getEmail()));
|
||||
$this->mailer->send($email->getMessage());
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getConnectorProvider(WatchList $watchList): ?object
|
||||
private function getConnectorProvider(Watchlist $watchlist): ?object
|
||||
{
|
||||
$connector = $watchList->getConnector();
|
||||
$connector = $watchlist->getConnector();
|
||||
if (null === $connector || null === $connector->getProvider()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Message\UpdateRdapServers;
|
||||
use App\Service\RDAPService;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Service\OfficialDataService;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
@ -16,8 +17,8 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
final readonly class UpdateRdapServersHandler
|
||||
{
|
||||
public function __construct(
|
||||
private RDAPService $RDAPService,
|
||||
private ParameterBagInterface $bag,
|
||||
private OfficialDataService $officialDataService,
|
||||
private ParameterBagInterface $bag, private DomainRepository $domainRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -39,8 +40,9 @@ final readonly class UpdateRdapServersHandler
|
||||
*/
|
||||
|
||||
try {
|
||||
$this->RDAPService->updateTldListIANA();
|
||||
$this->RDAPService->updateGTldListICANN();
|
||||
$this->officialDataService->updateTldListIANA();
|
||||
$this->officialDataService->updateGTldListICANN();
|
||||
$this->domainRepository->setDomainDeletedIfTldIsDeleted();
|
||||
} catch (\Throwable $throwable) {
|
||||
$throws[] = $throwable;
|
||||
}
|
||||
@ -50,7 +52,7 @@ final readonly class UpdateRdapServersHandler
|
||||
*/
|
||||
|
||||
try {
|
||||
$this->RDAPService->updateRDAPServersFromIANA();
|
||||
$this->officialDataService->updateRDAPServersFromIANA();
|
||||
} catch (\Throwable $throwable) {
|
||||
$throws[] = $throwable;
|
||||
}
|
||||
@ -60,13 +62,13 @@ final readonly class UpdateRdapServersHandler
|
||||
*/
|
||||
|
||||
try {
|
||||
$this->RDAPService->updateRDAPServersFromFile($this->bag->get('custom_rdap_servers_file'));
|
||||
$this->officialDataService->updateRDAPServersFromFile($this->bag->get('custom_rdap_servers_file'));
|
||||
} catch (\Throwable $throwable) {
|
||||
$throws[] = $throwable;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->RDAPService->updateRegistrarListIANA();
|
||||
$this->officialDataService->updateRegistrarListIANA();
|
||||
} catch (\Throwable $throwable) {
|
||||
$throws[] = $throwable;
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ namespace App\MessageHandler;
|
||||
use App\Message\ValidateConnectorCredentials;
|
||||
use App\Notifier\ValidateConnectorCredentialsErrorNotification;
|
||||
use App\Repository\ConnectorRepository;
|
||||
use App\Service\Connector\AbstractProvider;
|
||||
use App\Service\Provider\AbstractProvider;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
|
||||
69
src/Notifier/DomainStatusUpdateNotification.php
Normal file
69
src/Notifier/DomainStatusUpdateNotification.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifier;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\DomainStatus;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Notifier\Message\ChatMessage;
|
||||
use Symfony\Component\Notifier\Message\EmailMessage;
|
||||
use Symfony\Component\Notifier\Message\PushMessage;
|
||||
use Symfony\Component\Notifier\Notification\Notification;
|
||||
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
|
||||
use Symfony\Component\Notifier\Recipient\RecipientInterface;
|
||||
|
||||
class DomainStatusUpdateNotification extends DomainWatchdogNotification
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Address $sender,
|
||||
private readonly Domain $domain,
|
||||
private readonly DomainStatus $domainStatus,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function asChatMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?ChatMessage
|
||||
{
|
||||
$ldhName = $this->domain->getLdhName();
|
||||
$this->subject("Domain EPP status changed $ldhName")
|
||||
->content("Domain name $ldhName EPP status has been updated.")
|
||||
->importance(Notification::IMPORTANCE_HIGH);
|
||||
|
||||
return ChatMessage::fromNotification($this);
|
||||
}
|
||||
|
||||
public function asPushMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?PushMessage
|
||||
{
|
||||
$ldhName = $this->domain->getLdhName();
|
||||
$this->subject("Domain EPP status changed $ldhName")
|
||||
->content("Domain name $ldhName EPP status has been updated.")
|
||||
->importance(Notification::IMPORTANCE_HIGH);
|
||||
|
||||
return PushMessage::fromNotification($this);
|
||||
}
|
||||
|
||||
public function asEmailMessage(EmailRecipientInterface $recipient): EmailMessage
|
||||
{
|
||||
$ldhName = $this->domain->getLdhName();
|
||||
|
||||
$email = (new TemplatedEmail())
|
||||
->from($this->sender)
|
||||
->to($recipient->getEmail())
|
||||
->priority(Email::PRIORITY_HIGH)
|
||||
->subject("Domain EPP status changed $ldhName")
|
||||
->htmlTemplate('emails/success/domain_status_updated.html.twig')
|
||||
->locale('en')
|
||||
->context([
|
||||
'domain' => $this->domain,
|
||||
'domainStatus' => $this->domainStatus,
|
||||
]);
|
||||
|
||||
$email->getHeaders()
|
||||
->addTextHeader('In-Reply-To', "<$ldhName+updated@domain-watchdog>")
|
||||
->addTextHeader('References', "<$ldhName+updated@domain-watchdog>");
|
||||
|
||||
return new EmailMessage($email);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user