Merged master

This commit is contained in:
Vincent
2024-09-18 13:37:07 +02:00
139 changed files with 8418 additions and 2409 deletions

View File

@@ -4,10 +4,10 @@ import TextPage from "./pages/TextPage";
import DomainSearchPage from "./pages/search/DomainSearchPage";
import EntitySearchPage from "./pages/search/EntitySearchPage";
import NameserverSearchPage from "./pages/search/NameserverSearchPage";
import TldPage from "./pages/info/TldPage";
import StatisticsPage from "./pages/info/StatisticsPage";
import TldPage from "./pages/search/TldPage";
import StatisticsPage from "./pages/StatisticsPage";
import WatchlistPage from "./pages/tracking/WatchlistPage";
import UserPage from "./pages/watchdog/UserPage";
import UserPage from "./pages/UserPage";
import React, {useCallback, useEffect, useMemo, useState} from "react";
import {getUser} from "./utils/api";
import LoginPage, {AuthenticatedContext} from "./pages/LoginPage";
@@ -40,7 +40,8 @@ export default function App() {
const contextValue = useMemo(() => ({
authenticated,
setIsAuthenticated
}), [authenticated, setIsAuthenticated]);
}), [authenticated, setIsAuthenticated])
useEffect(() => {
getUser().then(() => {
@@ -77,13 +78,12 @@ export default function App() {
<Route path="/search/domain" element={<DomainSearchPage/>}/>
<Route path="/search/entity" element={<EntitySearchPage/>}/>
<Route path="/search/nameserver" element={<NameserverSearchPage/>}/>
<Route path="/info/tld" element={<TldPage/>}/>
<Route path="/info/stats" element={<StatisticsPage/>}/>
<Route path="/search/tld" element={<TldPage/>}/>
<Route path="/tracking/watchlist" element={<WatchlistPage/>}/>
<Route path="/tracking/connectors" element={<ConnectorsPage/>}/>
<Route path="/stats" element={<StatisticsPage/>}/>
<Route path="/user" element={<UserPage/>}/>
<Route path="/faq" element={<TextPage resource='faq.md'/>}/>
@@ -97,7 +97,7 @@ export default function App() {
</div>
</Layout.Content>
<Layout.Footer style={{textAlign: 'center'}}>
<Space size='middle'>
<Space size='middle' wrap align='center'>
<Link to='/tos'><Button type='text'>{t`TOS`}</Button></Link>
<Link to='/privacy'><Button type='text'>{t`Privacy Policy`}</Button></Link>
<Link to='/faq'><Button type='text'>{t`FAQ`}</Button></Link>

View File

@@ -4,7 +4,8 @@ import React, {useContext, useEffect} from "react";
import {getUser, login} from "../utils/api";
import {AuthenticatedContext} from "../pages/LoginPage";
import {useNavigate} from "react-router-dom";
import {showErrorAPI} from "../utils";
import {showErrorAPI} from "../utils/functions/showErrorAPI";
type FieldType = {

View File

@@ -3,7 +3,8 @@ import {t} from "ttag";
import React, {useState} from "react";
import {register} from "../utils/api";
import {useNavigate} from "react-router-dom";
import {showErrorAPI} from "../utils";
import {showErrorAPI} from "../utils/functions/showErrorAPI";
type FieldType = {

View File

@@ -1,4 +1,4 @@
import {ItemType, MenuItemType} from "antd/lib/menu/interface";
import {ItemType} from "antd/lib/menu/interface";
import {t} from "ttag";
import {
AimOutlined,
@@ -22,8 +22,7 @@ import {useNavigate} from "react-router-dom";
export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
const navigate = useNavigate()
const menuItems: ItemType<MenuItemType>[] = [
const menuItems: ItemType[] = [
{
key: 'home',
label: t`Home`,
@@ -49,7 +48,7 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
label: t`TLD`,
title: t`TLD list`,
disabled: !isAuthenticated,
onClick: () => navigate('/info/tld')
onClick: () => navigate('/search/tld')
},
{
key: 'entity-finder',
@@ -64,7 +63,8 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
icon: <CloudServerOutlined/>,
label: t`Nameserver`,
title: t`Nameserver Finder`,
disabled: true
disabled: true,
onClick: () => navigate('/search/nameserver')
}
]
},
@@ -93,8 +93,8 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
key: 'stats',
icon: <LineChartOutlined/>,
label: t`Statistics`,
disabled: true,
onClick: () => navigate('/info/stats')
disabled: !isAuthenticated,
onClick: () => navigate('/stats')
}
]
@@ -103,7 +103,6 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
key: 'account',
icon: <UserOutlined/>,
label: t`My Account`,
disabled: !isAuthenticated,
onClick: () => navigate('/user')
}, {
key: 'logout',
@@ -122,7 +121,6 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
}
return <Menu
defaultSelectedKeys={['home']}
defaultOpenKeys={['search', 'info', 'tracking', 'doc']}
mode="inline"
theme="dark"

View File

@@ -0,0 +1,47 @@
import React, {useEffect} from "react";
import {Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState} from "@xyflow/react";
import {Flex} from "antd";
import {Domain} from "../../utils/api";
import {getLayoutedElements} from "../tracking/watchlist/diagram/getLayoutedElements";
import {domainEntitiesToNode, domainToNode, nsToNode, tldToNode} from "../tracking/watchlist/diagram/watchlistToNodes";
import {domainEntitiesToEdges, domainNSToEdges, tldToEdge} from "../tracking/watchlist/diagram/watchlistToEdges";
export function DomainDiagram({domain}: { domain: Domain }) {
const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([])
useEffect(() => {
const e = getLayoutedElements([
domainToNode(domain),
...domainEntitiesToNode(domain, true),
tldToNode(domain.tld),
...domain.nameservers.map(nsToNode)
].flat(), [
domainEntitiesToEdges(domain, true),
tldToEdge(domain),
...domainNSToEdges(domain)
].flat())
setNodes(e.nodes)
setEdges(e.edges)
}, [])
return <Flex style={{width: '100%', height: '100vh'}}>
<ReactFlow
fitView
colorMode='dark'
nodesConnectable={false}
edgesReconnectable={false}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
style={{width: '100%', height: '100%'}}
>
<MiniMap/>
<Controls/>
<Background/>
</ReactFlow>
</Flex>
}

View File

@@ -0,0 +1,45 @@
import {StepProps, Steps, Tooltip} from "antd";
import React from "react";
import {t} from "ttag";
import {CheckOutlined, DeleteOutlined, ExclamationCircleOutlined, SignatureOutlined} from "@ant-design/icons";
import {rdapEventDetailTranslation, rdapStatusCodeDetailTranslation} from "../../utils/functions/rdapTranslation";
export function DomainLifecycleSteps({status}: { status: string[] }) {
const rdapEventDetailTranslated = rdapEventDetailTranslation()
const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation()
const steps: StepProps[] = [
{
title: <Tooltip title={rdapEventDetailTranslated.registration}>{t`Registration`}</Tooltip>,
icon: <SignatureOutlined style={{color: 'green'}}/>
},
{
title: <Tooltip title={rdapStatusCodeDetailTranslated.active}>{t`Active`}</Tooltip>,
icon: <CheckOutlined/>
},
{
title: <Tooltip
title={rdapStatusCodeDetailTranslated["redemption period"]}>{t`Redemption Period`}</Tooltip>,
icon: <ExclamationCircleOutlined style={{color: 'orangered'}}/>
},
{
title: <Tooltip title={rdapStatusCodeDetailTranslated["pending delete"]}>{t`Pending Delete`}</Tooltip>,
icon: <DeleteOutlined style={{color: 'palevioletred'}}/>
}
]
let currentStep = 1
if (status.includes('redemption period')) {
currentStep = 2
} else if (status.includes('pending delete')) {
currentStep = 3
}
return <Steps
current={currentStep}
items={steps}
/>
}

View File

@@ -0,0 +1,76 @@
import {Badge, Card, Divider, Flex, Space, Tag, Tooltip, Typography} from "antd";
import {t} from "ttag";
import {EventTimeline} from "./EventTimeline";
import {EntitiesList} from "./EntitiesList";
import {DomainDiagram} from "./DomainDiagram";
import React from "react";
import {Domain} from "../../utils/api";
import {rdapStatusCodeDetailTranslation} from "../../utils/functions/rdapTranslation";
import {regionNames} from "../../i18n";
import {getCountryCode} from "../../utils/functions/getCountryCode";
import {eppStatusCodeToColor} from "../../utils/functions/eppStatusCodeToColor";
import {DomainLifecycleSteps} from "./DomainLifecycleSteps";
export function DomainResult({domain}: { domain: Domain }) {
const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation()
const {tld, events} = domain
const domainEvents = events.sort((e1, e2) => new Date(e2.date).getTime() - new Date(e1.date).getTime())
return <Space direction="vertical" size="middle" style={{width: '100%'}}>
<Badge.Ribbon text={
<Tooltip
title={tld.type === 'ccTLD' ? regionNames.of(getCountryCode(tld.tld)) : tld.type === 'gTLD' ? tld?.registryOperator : undefined}>
{`.${domain.tld.tld.toUpperCase()} (${tld.type})`}
</Tooltip>
}
color={
tld.type === 'ccTLD' ? 'purple' :
(tld.type === 'gTLD' && tld.specification13) ? "volcano" :
tld.type === 'gTLD' ? "green"
: "cyan"
}>
<Card title={<Space>
{domain.ldhName}{domain.handle && <Typography.Text code>{domain.handle}</Typography.Text>}
</Space>}
size="small">
{
domain.events.length > 0 && <DomainLifecycleSteps status={domain.status}/>
}
{domain.status.length > 0 &&
<>
<Divider orientation="left">{t`EPP Status Codes`}</Divider>
<Flex gap="4px 0" wrap>
{
domain.status.map(s =>
<Tooltip
placement='bottomLeft'
title={s in rdapStatusCodeDetailTranslated ? rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] : undefined}>
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
</Tooltip>
)
}
</Flex>
</>
}
{
domain.events.length > 0 && <>
<Divider orientation="left">{t`Timeline`}</Divider>
<EventTimeline events={domainEvents}/>
</>
}
{
domain.entities.length > 0 &&
<>
<Divider orientation="left">{t`Entities`}</Divider>
<EntitiesList domain={domain}/>
</>
}
</Card>
</Badge.Ribbon>
<DomainDiagram domain={domain}/>
</Space>
}

View File

@@ -8,13 +8,10 @@ export type FieldType = {
}
export function DomainSearchBar({onFinish}: { onFinish: (values: FieldType) => void }) {
return <Form
name="basic"
labelCol={{span: 8}}
wrapperCol={{span: 16}}
onFinish={onFinish}
autoComplete="off"
style={{width: '100%'}}
>
<Form.Item<FieldType>
name="ldhName"
@@ -28,8 +25,13 @@ export function DomainSearchBar({onFinish}: { onFinish: (values: FieldType) => v
min: 2
}]}
>
<Input size="large" prefix={<SearchOutlined/>} placeholder="example.com" autoFocus={true}
autoComplete='off'/>
<Input style={{textAlign: 'center'}}
size="large"
prefix={<SearchOutlined/>}
placeholder="example.com"
autoComplete='off'
autoFocus
/>
</Form.Item>
</Form>
}

View File

@@ -1,53 +1,36 @@
import vCard from "vcf";
import {Avatar, List} from "antd";
import {BankOutlined, IdcardOutlined, SignatureOutlined, ToolOutlined, UserOutlined} from "@ant-design/icons";
import {List, Tag, Tooltip} from "antd";
import React from "react";
import {Domain} from "../../utils/api";
import {t} from "ttag";
import {rdapRoleDetailTranslation, rdapRoleTranslation} from "../../utils/functions/rdapTranslation";
import {roleToAvatar} from "../../utils/functions/roleToAvatar";
import {rolesToColor} from "../../utils/functions/rolesToColor";
import {entityToName} from "../../utils/functions/entityToName";
import {sortDomainEntities} from "../../utils/functions/sortDomainEntities";
export function EntitiesList({domain}: { domain: Domain }) {
const domainRole = {
registrant: t`Registrant`,
technical: t`Technical`,
administrative: t`Administrative`,
abuse: t`Abuse`,
billing: t`Billing`,
registrar: t`Registrar`,
reseller: t`Reseller`,
sponsor: t`Sponsor`,
proxy: t`Proxy`,
notifications: t`Notifications`,
noc: t`Noc`
}
const rdapRoleTranslated = rdapRoleTranslation()
const rdapRoleDetailTranslated = rdapRoleDetailTranslation()
const roleToTag = (r: string) => <Tooltip
title={r in rdapRoleDetailTranslated ? rdapRoleDetailTranslated[r as keyof typeof rdapRoleDetailTranslated] : undefined}>
<Tag
color={rolesToColor([r])}>{rdapRoleTranslated[r as keyof typeof rdapRoleTranslated]}</Tag>
</Tooltip>
return <List
className="demo-loadmore-list"
itemLayout="horizontal"
dataSource={domain.entities.sort((e1, e2) => {
const p = (r: string[]) => r.includes('registrant') ? 4 : r.includes('administrative') ? 3 : r.includes('billing') ? 2 : 1
return p(e2.roles) - p(e1.roles)
})}
renderItem={(e) => {
const jCard = vCard.fromJSON(e.entity.jCard)
let name = ''
if (jCard.data.fn !== undefined && !Array.isArray(jCard.data.fn)) name = jCard.data.fn.valueOf()
return <List.Item>
dataSource={sortDomainEntities(domain)}
renderItem={(e) =>
<List.Item>
<List.Item.Meta
avatar={<Avatar style={{backgroundColor: '#87d068'}}
icon={e.roles.includes('registrant') ?
<SignatureOutlined/> : e.roles.includes('registrar') ?
<BankOutlined/> :
e.roles.includes('technical') ?
<ToolOutlined/> :
e.roles.includes('administrative') ?
<IdcardOutlined/> :
<UserOutlined/>}/>}
avatar={roleToAvatar(e)}
title={e.entity.handle}
description={name}
description={entityToName(e)}
/>
<div>{e.roles.map((r) => Object.keys(domainRole).includes(r) ? domainRole[r as keyof typeof domainRole] : r).join(', ')}</div>
{e.roles.map(roleToTag)}
</List.Item>
}}
}
/>
}

View File

@@ -1,85 +1,52 @@
import {
ClockCircleOutlined,
DeleteOutlined,
ReloadOutlined,
ShareAltOutlined,
SignatureOutlined,
SyncOutlined
} from "@ant-design/icons";
import {Timeline} from "antd";
import {Timeline, Tooltip, Typography} from "antd";
import React from "react";
import {Domain, EventAction} from "../../utils/api";
import {t} from "ttag";
import {Event} from "../../utils/api";
import useBreakpoint from "../../hooks/useBreakpoint";
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../utils/functions/rdapTranslation";
import {actionToColor} from "../../utils/functions/actionToColor";
import {actionToIcon} from "../../utils/functions/actionToIcon";
export function actionToColor(a: EventAction) {
return a === 'registration' ? 'green' :
a === 'reregistration' ? 'cyan' :
a === 'expiration' ? 'red' :
a === 'deletion' ? 'magenta' :
a === 'transfer' ? 'orange' :
a === 'last changed' ? 'blue' : 'default'
}
export const domainEvent = () => ({
registration: t`Registration`,
reregistration: t`Reregistration`,
'last changed': t`Last changed`,
expiration: t`Expiration`,
deletion: t`Deletion`,
reinstantiation: t`Reinstantiation`,
transfer: t`Transfer`,
locked: t`Locked`,
unlocked: t`Unlocked`,
'registrar expiration': t`Registrar expiration`,
'enum validation expiration': t`ENUM validation expiration`
})
export function EventTimeline({domain}: { domain: Domain }) {
export function EventTimeline({events}: { events: Event[] }) {
const sm = useBreakpoint('sm')
const locale = navigator.language.split('-')[0]
const domainEventTranslated = domainEvent()
const rdapEventNameTranslated = rdapEventNameTranslation()
const rdapEventDetailTranslated = rdapEventDetailTranslation()
return <Timeline
mode={sm ? "left" : "right"}
items={domain.events
.sort((e1, e2) => new Date(e2.date).getTime() - new Date(e1.date).getTime())
.map(({action, date}) => {
let dot
if (action === 'registration') {
dot = <SignatureOutlined style={{fontSize: '16px'}}/>
} else if (action === 'expiration') {
dot = <ClockCircleOutlined style={{fontSize: '16px'}}/>
} else if (action === 'transfer') {
dot = <ShareAltOutlined style={{fontSize: '16px'}}/>
} else if (action === 'last changed') {
dot = <SyncOutlined style={{fontSize: '16px'}}/>
} else if (action === 'deletion') {
dot = <DeleteOutlined style={{fontSize: '16px'}}/>
} else if (action === 'reregistration') {
dot = <ReloadOutlined style={{fontSize: '16px'}}/>
}
return <>
<Timeline
mode={sm ? "left" : "right"}
items={events.map(e => {
const sameEvents = events.filter(se => se.action === e.action)
const eventName = Object.keys(domainEventTranslated).includes(action) ? domainEventTranslated[action as keyof typeof domainEventTranslated] : action
const dateStr = new Date(date).toLocaleString(locale)
const eventName = <Typography.Text style={{color: e.deleted ? 'grey' : 'default'}}>
{e.action in rdapEventNameTranslated ? rdapEventNameTranslated[e.action as keyof typeof rdapEventNameTranslated] : e.action}
</Typography.Text>
const dateStr = <Typography.Text
style={{color: e.deleted ? 'grey' : 'default'}}>{new Date(e.date).toLocaleString(locale)}
</Typography.Text>
const eventDetail = e.action in rdapEventDetailTranslated ? rdapEventDetailTranslated[e.action as keyof typeof rdapEventDetailTranslated] : undefined
const text = sm ? {
children: <>{eventName}&emsp;{dateStr}</>
children: <Tooltip placement='bottom' title={eventDetail}>
{eventName}&emsp;{dateStr}
</Tooltip>
} : {
label: dateStr,
children: eventName,
children: <Tooltip placement='left' title={eventDetail}>{eventName}</Tooltip>,
}
return {
color: actionToColor(action),
dot,
pending: new Date(date).getTime() > new Date().getTime(),
color: e.deleted ? 'grey' : actionToColor(e.action),
dot: actionToIcon(e.action),
pending: new Date(e.date).getTime() > new Date().getTime(),
...text
}
}
)
}
/>
}
/>
</>
}

View File

@@ -1,85 +0,0 @@
import {Card, Divider, Popconfirm, Space, Table, Tag, theme, Typography} from "antd";
import {t} from "ttag";
import {deleteWatchlist} from "../../utils/api";
import {CalendarFilled, DeleteFilled, DisconnectOutlined, LinkOutlined} from "@ant-design/icons";
import React from "react";
import useBreakpoint from "../../hooks/useBreakpoint";
import {actionToColor, domainEvent} from "../search/EventTimeline";
import {Watchlist} from "../../pages/tracking/WatchlistPage";
import punycode from "punycode/punycode";
const {useToken} = theme;
export function WatchlistsList({watchlists, onDelete}: { watchlists: Watchlist[], onDelete: () => void }) {
const {token} = useToken()
const sm = useBreakpoint('sm')
const domainEventTranslated = domainEvent()
const columns = [
{
title: t`Domain names`,
dataIndex: 'domains'
},
{
title: t`Tracked events`,
dataIndex: 'events'
}
]
return <>
{watchlists.map(watchlist =>
<>
<Card
hoverable
title={<>
{
watchlist.connector ?
<Tag icon={<LinkOutlined/>} color="lime-inverse" title={watchlist.connector.id}/> :
<Tag icon={<DisconnectOutlined/>} color="default"
title={t`This Watchlist is not linked to a Connector.`}/>
}
<Typography.Text title={new Date(watchlist.createdAt).toLocaleString()}>
{t`Watchlist` + (watchlist.name ? ` (${watchlist.name})` : '')}
</Typography.Text>
</>
}
size='small'
style={{width: '100%'}}
extra={<Space size='middle'>
<Typography.Link href={`/api/watchlists/${watchlist.token}/calendar`}>
<CalendarFilled title={t`Export events to iCalendar format`}/>
</Typography.Link>
<Popconfirm
title={t`Delete the Watchlist`}
description={t`Are you sure to delete this Watchlist?`}
onConfirm={() => deleteWatchlist(watchlist.token).then(onDelete)}
okText={t`Yes`}
cancelText={t`No`}
okButtonProps={{danger: true}}>
<DeleteFilled style={{color: token.colorError}} title={t`Delete the Watchlist`}/>
</Popconfirm>
</Space>}
>
<Card.Meta description={watchlist.token} style={{marginBottom: '1em'}}/>
<Table
size='small'
columns={columns}
pagination={false}
style={{width: '100%'}}
dataSource={[{
domains: watchlist.domains.map(d => <Tag>{punycode.toUnicode(d.ldhName)}</Tag>),
events: watchlist.triggers?.filter(t => t.action === 'email')
.map(t => <Tag color={actionToColor(t.event)}>
{domainEventTranslated[t.event as keyof typeof domainEventTranslated]}
</Tag>
)
}]}
{...(sm ? {scroll: {y: 'max-content'}} : {scroll: {y: 240}})}
/>
</Card>
<Divider/>
</>
)}
</>
}

View File

@@ -1,6 +1,6 @@
import {Button, Checkbox, Form, FormInstance, Input, Popconfirm, Select, Space, Typography} from "antd";
import React, {useState} from "react";
import {Connector, ConnectorProvider} from "../../utils/api/connectors";
import {Connector, ConnectorProvider} from "../../../utils/api/connectors";
import {t} from "ttag";
import {BankOutlined} from "@ant-design/icons";
import {
@@ -8,15 +8,15 @@ import {
ovhFields as ovhFieldsFunction,
ovhPricingMode as ovhPricingModeFunction,
ovhSubsidiaryList as ovhSubsidiaryListFunction
} from "../../utils/providers/ovh";
import {helpGetTokenLink, tosHyperlink} from "../../utils/providers";
} from "../../../utils/providers/ovh";
import {helpGetTokenLink, tosHyperlink} from "../../../utils/providers";
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: {span: 24, offset: 0},
sm: {span: 20, offset: 4},
},
};
}
export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate: (values: Connector) => void }) {
const [provider, setProvider] = useState<string>()
@@ -186,7 +186,7 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
</>
}
<Form.Item style={{marginTop: 10}}>
<Form.Item style={{marginTop: '5vh'}}>
<Space>
<Button type="primary" htmlType="submit">
{t`Create`}

View File

@@ -1,8 +1,8 @@
import {Card, Divider, Popconfirm, theme, Typography} from "antd";
import {Card, Divider, message, Popconfirm, theme, Typography} from "antd";
import {t} from "ttag";
import {DeleteFilled} from "@ant-design/icons";
import React from "react";
import {Connector, deleteConnector} from "../../utils/api/connectors";
import {Connector, deleteConnector} from "../../../utils/api/connectors";
const {useToken} = theme;
@@ -11,17 +11,23 @@ export type ConnectorElement = Connector & { id: string, createdAt: string }
export function ConnectorsList({connectors, onDelete}: { connectors: ConnectorElement[], onDelete: () => void }) {
const {token} = useToken()
const [messageApi, contextHolder] = message.useMessage()
const onConnectorDelete = (connector: ConnectorElement) => deleteConnector(connector.id)
.then(onDelete)
.catch(() => messageApi.error(t`An error occurred while deleting the Connector. Make sure it is not used in any Watchlist`))
return <>
{connectors.map(connector =>
<>
{contextHolder}
<Card hoverable title={<Typography.Text
title={new Date(connector.createdAt).toLocaleString()}>{t`Connector ${connector.provider}`}</Typography.Text>}
size='small'
style={{width: '100%'}}
extra={<Popconfirm title={t`Delete the Connector`}
description={t`Are you sure to delete this Connector?`}
onConfirm={() => deleteConnector(connector.id).then(onDelete)}
onConfirm={() => onConnectorDelete(connector)}
okText={t`Yes`}
cancelText={t`No`}
><DeleteFilled style={{color: token.colorError}}/></Popconfirm>}>

View File

@@ -0,0 +1,22 @@
import {CalendarFilled} from "@ant-design/icons";
import {t} from "ttag";
import {Popover, QRCode, Typography} from "antd";
import React from "react";
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
export function CalendarWatchlistButton({watchlist}: { watchlist: Watchlist }) {
const icsResourceLink = `${window.location.origin}/api/watchlists/${watchlist.token}/calendar`
return <Typography.Link href={icsResourceLink}>
<Popover content={<QRCode value={icsResourceLink}
bordered={false}
title={t`QR Code for iCalendar export`}
type='svg'
/>}>
<CalendarFilled title={t`Export events to iCalendar format`}
style={{color: 'limegreen'}}
/>
</Popover>
</Typography.Link>
}

View File

@@ -0,0 +1,22 @@
import {Popconfirm, theme, Typography} from "antd";
import {t} from "ttag";
import {deleteWatchlist} from "../../../utils/api";
import {DeleteFilled} from "@ant-design/icons";
import React from "react";
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
export function DeleteWatchlistButton({watchlist, onDelete}: { watchlist: Watchlist, onDelete: () => void }) {
const {token} = theme.useToken()
return <Popconfirm
title={t`Delete the Watchlist`}
description={t`Are you sure to delete this Watchlist?`}
onConfirm={() => deleteWatchlist(watchlist.token).then(onDelete)}
okText={t`Yes`}
cancelText={t`No`}
okButtonProps={{danger: true}}>
<Typography.Link>
<DeleteFilled style={{color: token.colorError}} title={t`Delete the Watchlist`}/>
</Typography.Link>
</Popconfirm>
}

View File

@@ -0,0 +1,79 @@
import React, {useEffect, useState} from "react";
import {Domain, getTrackedDomainList} from "../../../utils/api";
import {Table, Tag, Tooltip} from "antd";
import {t} from "ttag";
import useBreakpoint from "../../../hooks/useBreakpoint";
import {ColumnType} from "antd/es/table";
import {rdapStatusCodeDetailTranslation} from "../../../utils/functions/rdapTranslation";
import {eppStatusCodeToColor} from "../../../utils/functions/eppStatusCodeToColor";
export function TrackedDomainTable() {
const sm = useBreakpoint('sm')
const [dataTable, setDataTable] = useState<Domain[]>([])
const [total, setTotal] = useState()
const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation()
const fetchData = (params: { page: number, itemsPerPage: number }) => {
getTrackedDomainList(params).then(data => {
setTotal(data['hydra:totalItems'])
setDataTable(data['hydra:member'].map((d: Domain) => {
const expirationDate = d.events.find(e => e.action === 'expiration' && !e.deleted)?.date
return {
key: d.ldhName,
ldhName: d.ldhName,
expirationDate: expirationDate ? new Date(expirationDate).toLocaleString() : '-',
status: d.status.map(s => <Tooltip
placement='bottomLeft'
title={s in rdapStatusCodeDetailTranslated ? rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] : undefined}>
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
</Tooltip>
),
updatedAt: new Date(d.updatedAt).toLocaleString()
}
}))
})
}
useEffect(() => {
fetchData({page: 1, itemsPerPage: 30})
}, [])
const columns: ColumnType<any>[] = [
{
title: t`Domain`,
dataIndex: "ldhName"
},
{
title: t`Expiration date`,
dataIndex: 'expirationDate'
},
{
title: t`Status`,
dataIndex: 'status'
},
{
title: t`Updated at`,
dataIndex: 'updatedAt'
}
]
return <Table
loading={total === undefined}
columns={columns}
dataSource={dataTable}
pagination={{
total,
hideOnSinglePage: true,
defaultPageSize: 30,
onChange: (page, itemsPerPage) => {
fetchData({page, itemsPerPage})
}
}}
{...(sm ? {scroll: {y: 'max-content'}} : {scroll: {y: 240}})}
/>
}

View File

@@ -0,0 +1,68 @@
import {Button, Drawer, Form, Typography} from "antd";
import {t} from "ttag";
import {WatchlistForm} from "./WatchlistForm";
import React, {useState} from "react";
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
import {EditOutlined} from "@ant-design/icons";
import {Connector} from "../../../utils/api/connectors";
export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}: {
watchlist: Watchlist,
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
connectors: (Connector & { id: string })[]
}) {
const [form] = Form.useForm()
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const showDrawer = () => {
setOpen(true)
}
const onClose = () => {
setOpen(false)
setLoading(false)
}
return <>
<Typography.Link>
<EditOutlined title={t`Edit the Watchlist`} onClick={() => {
showDrawer()
form.setFields([
{name: 'token', value: watchlist.token},
{name: 'name', value: watchlist.name},
{name: 'connector', value: watchlist.connector?.id},
{name: 'domains', value: watchlist.domains.map(d => d.ldhName)},
{name: 'triggers', value: [...new Set(watchlist.triggers?.map(t => t.event))]},
{name: 'dsn', value: watchlist.dsn}
])
}}/>
</Typography.Link>
<Drawer
title={t`Update a Watchlist`}
width='80%'
onClose={onClose}
open={open}
loading={loading}
styles={{
body: {
paddingBottom: 80,
}
}}
extra={<Button onClick={onClose}>{t`Cancel`}</Button>}
>
<WatchlistForm
form={form}
onFinish={values => {
setLoading(true)
onUpdateWatchlist(values).then(onClose).catch(() => setLoading(false))
}}
connectors={connectors}
isCreation={false}
/>
</Drawer>
</>
}

View File

@@ -0,0 +1,96 @@
import {Card, Divider, Space, Table, Tag, Tooltip} from "antd";
import {DisconnectOutlined, LinkOutlined} from "@ant-design/icons";
import {t} from "ttag";
import {ViewDiagramWatchlistButton} from "./diagram/ViewDiagramWatchlistButton";
import {UpdateWatchlistButton} from "./UpdateWatchlistButton";
import {DeleteWatchlistButton} from "./DeleteWatchlistButton";
import punycode from "punycode/punycode";
import React from "react";
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
import {Connector} from "../../../utils/api/connectors";
import useBreakpoint from "../../../hooks/useBreakpoint";
import {CalendarWatchlistButton} from "./CalendarWatchlistButton";
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../../utils/functions/rdapTranslation";
import {actionToColor} from "../../../utils/functions/actionToColor";
export function WatchlistCard({watchlist, onUpdateWatchlist, connectors, onDelete}: {
watchlist: Watchlist,
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
connectors: (Connector & { id: string })[],
onDelete: () => void
}) {
const sm = useBreakpoint('sm')
const rdapEventNameTranslated = rdapEventNameTranslation()
const rdapEventDetailTranslated = rdapEventDetailTranslation()
const columns = [
{
title: t`Domain names`,
dataIndex: 'domains'
},
{
title: t`Tracked events`,
dataIndex: 'events'
}
]
return <>
<Card
type='inner'
title={<>
{
watchlist.connector ?
<Tooltip title={watchlist.connector.id}>
<Tag icon={<LinkOutlined/>} color="lime-inverse"/>
</Tooltip> :
<Tooltip title={t`This Watchlist is not linked to a Connector.`}>
<Tag icon={<DisconnectOutlined/>} color="default"/>
</Tooltip>
}
<Tooltip title={new Date(watchlist.createdAt).toLocaleString()}>
{t`Watchlist` + (watchlist.name ? ` (${watchlist.name})` : '')}
</Tooltip>
</>
}
size='small'
style={{width: '100%'}}
extra={
<Space size='middle'>
<ViewDiagramWatchlistButton token={watchlist.token}/>
<CalendarWatchlistButton watchlist={watchlist}/>
<UpdateWatchlistButton
watchlist={watchlist}
onUpdateWatchlist={onUpdateWatchlist}
connectors={connectors}
/>
<DeleteWatchlistButton watchlist={watchlist} onDelete={onDelete}/>
</Space>
}
>
<Card.Meta description={watchlist.token} style={{marginBottom: '1em'}}/>
<Table
size='small'
columns={columns}
pagination={false}
style={{width: '100%'}}
dataSource={[{
domains: watchlist.domains.map(d => <Tag>{punycode.toUnicode(d.ldhName)}</Tag>),
events: watchlist.triggers?.filter(t => t.action === 'email')
.map(t => <Tooltip
title={t.event in rdapEventDetailTranslated ? rdapEventDetailTranslated[t.event as keyof typeof rdapEventDetailTranslated] : undefined}>
<Tag color={actionToColor(t.event)}>
{rdapEventNameTranslated[t.event as keyof typeof rdapEventNameTranslated]}
</Tag>
</Tooltip>
)
}]}
{...(sm ? {scroll: {y: 'max-content'}} : {scroll: {y: 240}})}
/>
</Card>
<Divider/>
</>
}

View File

@@ -1,9 +1,11 @@
import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag} from "antd";
import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag, Tooltip, Typography} from "antd";
import {t} from "ttag";
import {ApiOutlined, MinusCircleOutlined, PlusOutlined} from "@ant-design/icons";
import React from "react";
import {Connector} from "../../utils/api/connectors";
import {actionToColor, domainEvent} from "../search/EventTimeline";
import {Connector} from "../../../utils/api/connectors";
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../../utils/functions/rdapTranslation";
import {actionToColor} from "../../../utils/functions/actionToColor";
import {actionToIcon} from "../../../utils/functions/actionToIcon";
type TagRender = SelectProps['tagRender'];
@@ -25,38 +27,48 @@ const formItemLayoutWithOutLabel = {
},
};
export function WatchlistForm({form, connectors, onCreateWatchlist}: {
export function WatchlistForm({form, connectors, onFinish, isCreation}: {
form: FormInstance,
connectors: (Connector & { id: string })[]
onCreateWatchlist: (values: { domains: string[], emailTriggers: string[] }) => void
onFinish: (values: { domains: string[], triggers: string[], token: string }) => void
isCreation: boolean
}) {
const domainEventTranslated = domainEvent()
const rdapEventNameTranslated = rdapEventNameTranslation()
const rdapEventDetailTranslated = rdapEventDetailTranslation()
const triggerTagRenderer: TagRender = (props) => {
const {value, closable, onClose} = props;
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
event.stopPropagation();
};
return (
<Tag
color={actionToColor(value)}
onMouseDown={onPreventMouseDown}
closable={closable}
onClose={onClose}
style={{marginInlineEnd: 4}}
>
{domainEventTranslated[value as keyof typeof domainEventTranslated]}
</Tag>
event.preventDefault()
event.stopPropagation()
}
return (<Tooltip
title={value in rdapEventDetailTranslated ? rdapEventDetailTranslated[value as keyof typeof rdapEventDetailTranslated] : undefined}>
<Tag
icon={actionToIcon(value)}
color={actionToColor(value)}
onMouseDown={onPreventMouseDown}
closable={closable}
onClose={onClose}
style={{marginInlineEnd: 4}}
>
{rdapEventNameTranslated[value as keyof typeof rdapEventNameTranslated]}
</Tag>
</Tooltip>
)
}
return <Form
{...formItemLayoutWithOutLabel}
form={form}
onFinish={onCreateWatchlist}
initialValues={{emailTriggers: ['last changed', 'transfer', 'expiration', 'deletion']}}
onFinish={onFinish}
initialValues={{triggers: ['last changed', 'transfer', 'expiration', 'deletion']}}
>
<Form.Item name='token' hidden>
<Input hidden/>
</Form.Item>
<Form.Item label={t`Name`}
name='name'
labelCol={{
@@ -134,7 +146,7 @@ export function WatchlistForm({form, connectors, onCreateWatchlist}: {
)}
</Form.List>
<Form.Item label={t`Tracked events`}
name='emailTriggers'
name='triggers'
rules={[{required: true, message: t`At least one trigger`, type: 'array'}]}
labelCol={{
xs: {span: 24},
@@ -150,9 +162,10 @@ export function WatchlistForm({form, connectors, onCreateWatchlist}: {
mode="multiple"
tagRender={triggerTagRenderer}
style={{width: '100%'}}
options={Object.keys(domainEventTranslated).map(e => ({
options={Object.keys(rdapEventNameTranslated).map(e => ({
value: e,
label: domainEventTranslated[e as keyof typeof domainEventTranslated]
title: e in rdapEventDetailTranslated ? rdapEventDetailTranslated[e as keyof typeof rdapEventDetailTranslated] : undefined,
label: rdapEventNameTranslated[e as keyof typeof rdapEventNameTranslated]
}))}
/>
</Form.Item>
@@ -180,10 +193,63 @@ export function WatchlistForm({form, connectors, onCreateWatchlist}: {
}))}
/>
</Form.Item>
<Form.Item>
<Form.List
name="dsn">
{(fields, {add, remove}, {errors}) => (
<>
{fields.map((field, index) => (
<Form.Item
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
label={index === 0 ? t`DSN` : ''}
required={true}
key={field.key}
>
<Form.Item
{...field}
validateTrigger={['onChange', 'onBlur']}
rules={[{
required: true,
message: t`Required`
}, {
pattern: /:\/\//,
message: t`This DSN does not appear to be valid`
}]}
noStyle
>
<Input placeholder={'slack://TOKEN@default?channel=CHANNEL'} style={{width: '60%'}}
autoComplete='off'/>
</Form.Item>
{fields.length > 0 ? (
<MinusCircleOutlined
className="dynamic-delete-button"
onClick={() => remove(field.name)}
/>
) : null}
</Form.Item>
))}
<Form.Item help={
<Typography.Link href='https://symfony.com/doc/current/notifier.html#chat-channel'
target='_blank'>
{t`Check out this link to the Symfony documentation to help you build the DSN`}
</Typography.Link>}
>
<Button
type="dashed"
onClick={() => add()}
style={{width: '60%'}}
icon={<PlusOutlined/>}
>
{t`Add a Webhook`}
</Button>
<Form.ErrorList errors={errors}/>
</Form.Item>
</>
)}
</Form.List>
<Form.Item style={{marginTop: '5vh'}}>
<Space>
<Button type="primary" htmlType="submit">
{t`Create`}
{isCreation ? t`Create` : t`Update`}
</Button>
<Button type="default" htmlType="reset">
{t`Reset`}

View File

@@ -0,0 +1,23 @@
import React from "react";
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
import {Connector} from "../../../utils/api/connectors";
import {WatchlistCard} from "./WatchlistCard";
export function WatchlistsList({watchlists, onDelete, onUpdateWatchlist, connectors}: {
watchlists: Watchlist[],
onDelete: () => void,
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
connectors: (Connector & { id: string })[]
}) {
return <>
{watchlists.map(watchlist =>
<WatchlistCard watchlist={watchlist}
onUpdateWatchlist={onUpdateWatchlist}
connectors={connectors}
onDelete={onDelete}/>
)
}
</>
}

View File

@@ -0,0 +1,81 @@
import {Button, Flex, Modal, Space, Typography} from "antd"
import {t} from "ttag"
import React, {useEffect, useState} from "react"
import {ApartmentOutlined} from "@ant-design/icons"
import '@xyflow/react/dist/style.css'
import {Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState} from "@xyflow/react";
import {getWatchlist} from "../../../../utils/api";
import {getLayoutedElements} from "./getLayoutedElements";
import {watchlistToNodes} from "./watchlistToNodes";
import {watchlistToEdges} from "./watchlistToEdges";
export function ViewDiagramWatchlistButton({token}: { token: string }) {
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([])
useEffect(() => {
setEdges([])
setNodes([])
}, [])
useEffect(() => {
if (!open) return
setLoading(true)
getWatchlist(token).then(w => {
const e = getLayoutedElements(watchlistToNodes(w, true), watchlistToEdges(w, true))
setNodes(e.nodes)
setEdges(e.edges)
}).catch(() => setOpen(false)).finally(() => setLoading(false))
}, [open])
return <>
<Typography.Link>
<ApartmentOutlined title={t`View the Watchlist Entity Diagram`}
style={{color: 'darkviolet'}}
onClick={() => setOpen(true)}/>
</Typography.Link>
<Modal
title={t`Watchlist Entity Diagram`}
centered
open={open}
loading={loading}
footer={
<Space>
<Button type="default" onClick={() => setOpen(false)}>
Close
</Button>
</Space>
}
onOk={() => setOpen(false)}
onCancel={() => setOpen(false)}
width='90vw'
height='100%'
>
<Flex style={{width: '85vw', height: '85vh'}}>
<ReactFlow
fitView
colorMode='dark'
defaultEdges={[]}
defaultNodes={[]}
nodesConnectable={false}
edgesReconnectable={false}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
style={{width: '100%', height: '100%'}}
>
<MiniMap/>
<Controls/>
<Background/>
</ReactFlow>
</Flex>
</Modal>
</>
}

View File

@@ -0,0 +1,38 @@
import dagre from "dagre"
export const getLayoutedElements = (nodes: any, edges: any, direction = 'TB') => {
const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({}))
const nodeWidth = 172
const nodeHeight = 200
const isHorizontal = direction === 'LR';
dagreGraph.setGraph({rankdir: direction});
nodes.forEach((node: any) => {
dagreGraph.setNode(node.id, {width: nodeWidth, height: nodeHeight});
});
edges.forEach((edge: any) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
const newNodes = nodes.map((node: any) => {
const nodeWithPosition = dagreGraph.node(node.id)
return {
...node,
targetPosition: isHorizontal ? 'left' : 'top',
sourcePosition: isHorizontal ? 'right' : 'bottom',
position: {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2
},
};
});
return {nodes: newNodes, edges};
}

View File

@@ -0,0 +1,46 @@
import {Domain, Watchlist} from "../../../../utils/api";
import {rdapRoleTranslation} from "../../../../utils/functions/rdapTranslation";
import {t} from "ttag";
import {rolesToColor} from "../../../../utils/functions/rolesToColor";
export function domainEntitiesToEdges(d: Domain, withRegistrar = false) {
const rdapRoleTranslated = rdapRoleTranslation()
return d.entities
.filter(e => !e.deleted && (!withRegistrar ? !e.roles.includes('registrar') : true))
.map(e => ({
id: `e-${d.ldhName}-${e.entity.handle}`,
source: e.roles.includes('registrant') || e.roles.includes('registrar') ? e.entity.handle : d.ldhName,
target: e.roles.includes('registrant') || e.roles.includes('registrar') ? d.ldhName : e.entity.handle,
style: {stroke: rolesToColor(e.roles), strokeWidth: 3},
label: e.roles
.map(r => r in rdapRoleTranslated ? rdapRoleTranslated[r as keyof typeof rdapRoleTranslated] : r)
.join(', '),
animated: e.roles.includes('registrant'),
}))
}
export const domainNSToEdges = (d: Domain) => d.nameservers
.map(ns => ({
id: `ns-${d.ldhName}-${ns.ldhName}`,
source: d.ldhName,
target: ns.ldhName,
style: {stroke: 'grey', strokeWidth: 3},
label: 'DNS'
}))
export const tldToEdge = (d: Domain) => ({
id: `tld-${d.ldhName}-${d.tld.tld}`,
source: d.tld.tld,
target: d.ldhName,
style: {stroke: 'yellow', strokeWidth: 3},
label: t`Registry`
})
export function watchlistToEdges(watchlist: Watchlist, withRegistrar = false, withTld = false) {
const entitiesEdges = watchlist.domains.map(d => domainEntitiesToEdges(d, withRegistrar)).flat()
const nameserversEdges = watchlist.domains.map(domainNSToEdges).flat()
const tldEdge = watchlist.domains.map(tldToEdge)
return [...entitiesEdges, ...nameserversEdges, ...(withTld ? tldEdge : [])]
}

View File

@@ -0,0 +1,54 @@
import {Domain, Nameserver, Tld, Watchlist} from "../../../../utils/api";
import React from "react";
import {t} from 'ttag'
import {entityToName} from "../../../../utils/functions/entityToName";
export const domainToNode = (d: Domain) => ({
id: d.ldhName,
data: {label: <b>{d.ldhName}</b>},
style: {
width: 200
}
})
export const domainEntitiesToNode = (d: Domain, withRegistrar = false) => d.entities
.filter(e => !e.deleted && (!withRegistrar ? !e.roles.includes('registrar') : true))
.map(e => {
return {
id: e.entity.handle,
type: e.roles.includes('registrant') || e.roles.includes('registrar') ? 'input' : 'output',
data: {label: entityToName(e)},
style: {
width: 200
}
}
})
export const tldToNode = (tld: Tld) => ({
id: tld.tld,
data: {label: t`.${tld.tld} Registry`},
type: 'input',
style: {
width: 200
}
})
export const nsToNode = (ns: Nameserver) => ({
id: ns.ldhName,
data: {label: ns.ldhName},
type: 'output',
style: {
width: 200
}
})
export function watchlistToNodes(watchlist: Watchlist, withRegistrar = false, withTld = false) {
const domains = watchlist.domains.map(domainToNode)
const entities = [...new Set(watchlist.domains.map(d => domainEntitiesToNode(d, withRegistrar)).flat())]
const tlds = [...new Set(watchlist.domains.map(d => d.tld))].map(tldToNode)
const nameservers = [...new Set(watchlist.domains.map(d => d.nameservers))].flat().map(nsToNode, withRegistrar)
return [...domains, ...entities, ...nameservers, ...(withTld ? tlds : [])]
}

View File

@@ -5,10 +5,10 @@ export const regionNames = new Intl.DisplayNames([locale], {type: 'region'})
if (locale !== 'en') {
fetch(`/locales/${locale}.po.json`).then(response => {
if (!response.ok) throw new Error(`Failed to load translations for locale ${locale}`);
if (!response.ok) throw new Error(`Failed to load translations for locale ${locale}`)
response.json().then(translationsObj => {
addLocale(locale, translationsObj);
useLocale(locale);
addLocale(locale, translationsObj)
useLocale(locale)
})
})
}).catch(() => console.error(`Unable to retrieve translation file ${locale}.po.json`))
}

View File

@@ -7,11 +7,6 @@ import {getConfiguration, InstanceConfig} from "../utils/api";
import {RegisterForm} from "../components/RegisterForm";
const gridStyle: React.CSSProperties = {
width: '50%',
textAlign: 'center',
}
export const AuthenticatedContext = createContext<any>(null)
export default function LoginPage() {
@@ -28,7 +23,7 @@ export default function LoginPage() {
}, [])
return <Card title={wantRegister ? t`Register` : t`Log in`} style={{width: '100%'}}>
<Card.Grid style={gridStyle} hoverable={false}>
<Card.Grid style={{width: '50%', textAlign: 'center'}} hoverable={false}>
{wantRegister ? <RegisterForm/> : <LoginForm ssoLogin={configuration?.ssoLogin}/>}
{
configuration?.registerEnabled &&

View File

@@ -0,0 +1,120 @@
import React, {useEffect, useState} from "react";
import {getStatistics, Statistics} from "../utils/api";
import {Card, Col, Divider, Row, Statistic, Tooltip} from "antd";
import {t} from "ttag";
import {
AimOutlined,
CompassOutlined,
DatabaseOutlined,
FieldTimeOutlined,
NotificationOutlined
} from "@ant-design/icons";
export default function StatisticsPage() {
const [stats, setStats] = useState<Statistics>()
useEffect(() => {
getStatistics().then(setStats)
}, [])
const totalDomainPurchase = (stats?.domainPurchased ?? 0) + (stats?.domainPurchaseFailed ?? 0)
const successRate = stats !== undefined ?
(totalDomainPurchase === 0 ? undefined : stats.domainPurchased / totalDomainPurchase)
: undefined
return <>
<Row gutter={16}>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
prefix={<CompassOutlined/>}
title={t`RDAP queries`}
value={stats?.rdapQueries}
/>
</Card>
</Col>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={t`Alerts sent`}
prefix={<NotificationOutlined/>}
value={stats?.alertSent}
valueStyle={{color: 'blueviolet'}}
/>
</Card>
</Col>
</Row>
<Divider/>
<Row gutter={16}>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={t`Domain names in database`}
prefix={<DatabaseOutlined/>}
value={stats?.domainCountTotal}
valueStyle={{color: 'darkblue'}}
/>
</Card>
</Col>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={t`Tracked domain names`}
prefix={<AimOutlined/>}
value={stats?.domainTracked}
valueStyle={{color: 'darkviolet'}}
/>
</Card>
</Col>
</Row>
<Divider/>
<Row gutter={16}>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={t`Purchased domain names`}
prefix={<FieldTimeOutlined/>}
value={stats?.domainPurchased}
valueStyle={{color: '#3f8600'}}
/>
</Card>
</Col>
<Col span={12}>
<Card bordered={false}>
<Tooltip
title={t`This value is based on the status code of the HTTP response from the providers following the domain order.`}>
<Statistic
loading={stats === undefined}
title={t`Success rate`}
value={successRate === undefined ? '-' : successRate * 100}
suffix='%'
valueStyle={{color: successRate === undefined ? 'grey' : successRate >= 0.5 ? 'darkgreen' : 'orange'}}
/>
</Tooltip>
</Card>
</Col>
</Row>
<Divider/>
<Row gutter={16} justify='center' align='middle'>
{stats?.domainCount
.sort((a, b) => b.domain - a.domain)
.map(({domain, tld}) => <Col span={4}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={`.${tld}`}
value={domain}
valueStyle={{color: 'darkblue'}}
/>
</Card>
</Col>)}
</Row>
</>
}

View File

@@ -1,16 +1,29 @@
import React, {useEffect, useState} from "react";
import snarkdown from "snarkdown"
import {Skeleton} from "antd";
import {Skeleton, Typography} from "antd";
import axios from "axios";
import {t} from "ttag";
export default function TextPage({resource}: { resource: string }) {
const [markdown, setMarkdown] = useState<string>()
const [loading, setLoading] = useState<boolean>(false)
const [markdown, setMarkdown] = useState<string | undefined>(undefined)
useEffect(() => {
axios.get('/content/' + resource).then(res => setMarkdown(res.data))
setLoading(true)
axios.get('/content/' + resource)
.then(res => setMarkdown(res.data))
.catch(err => {
console.error(`Please create the /public/content/${resource} file.`)
setMarkdown(undefined)
})
.finally(() => setLoading(false))
}, [resource])
return <Skeleton loading={markdown === undefined} active>
{markdown !== undefined && <div dangerouslySetInnerHTML={{__html: snarkdown(markdown)}}></div>}
return <Skeleton loading={loading} active>
{markdown !== undefined ? <div
dangerouslySetInnerHTML={{__html: snarkdown(markdown)}}></div> :
<Typography.Text strong>
{t`📝 Please create the /public/content/${resource} file.`}
</Typography.Text>}
</Skeleton>
}

View File

@@ -1,6 +1,6 @@
import React, {useEffect, useState} from "react";
import {Card, Flex, Skeleton, Typography} from "antd";
import {getUser, User} from "../../utils/api";
import {getUser, User} from "../utils/api";
import {t} from 'ttag'
export default function UserPage() {

View File

@@ -1,7 +0,0 @@
import React from "react";
export default function StatisticsPage() {
return <p>
Not implemented
</p>
}

View File

@@ -1,14 +1,11 @@
import React, {useState} from "react";
import {Badge, Card, Divider, Empty, Flex, FormProps, message, Skeleton, Space, Tag, Typography} from "antd";
import {Empty, Flex, FormProps, message, Skeleton} from "antd";
import {Domain, getDomain} from "../../utils/api";
import {AxiosError} from "axios"
import {t} from 'ttag'
import {DomainSearchBar, FieldType} from "../../components/search/DomainSearchBar";
import {EventTimeline} from "../../components/search/EventTimeline";
import {EntitiesList} from "../../components/search/EntitiesList";
import {showErrorAPI} from "../../utils";
const {Text} = Typography;
import {DomainResult} from "../../components/search/DomainResult";
import {showErrorAPI} from "../../utils/functions/showErrorAPI";
export default function DomainSearchPage() {
const [domain, setDomain] = useState<Domain | null>()
@@ -26,53 +23,16 @@ export default function DomainSearchPage() {
}
return <Flex gap="middle" align="center" justify="center" vertical>
<Card title={t`Domain finder`} style={{width: '100%'}}>
{contextHolder}
<DomainSearchBar onFinish={onFinish}/>
{contextHolder}
<DomainSearchBar onFinish={onFinish}/>
<Skeleton loading={domain === null} active>
{
domain &&
(!domain.deleted ? <Space direction="vertical" size="middle" style={{width: '100%'}}>
<Badge.Ribbon text={`.${domain.tld.tld.toUpperCase()} (${domain.tld.type})`}
color={
domain.tld.type === 'ccTLD' ? 'purple' :
(domain.tld.type === 'gTLD' && domain.tld.specification13) ? "volcano" :
domain.tld.type === 'gTLD' ? "green"
: "cyan"
}>
<Card title={<>
{domain.ldhName}{domain.handle && <Text code>{domain.handle}</Text>}
</>}
size="small">
{domain.status.length > 0 &&
<>
<Divider orientation="left">{t`EPP Status Codes`}</Divider>
<Flex gap="4px 0" wrap>
{
domain.status.map(s =>
<Tag color={s === 'active' ? 'green' : 'blue'}>{s}</Tag>
)
}
</Flex>
</>
}
<Divider orientation="left">{t`Timeline`}</Divider>
<EventTimeline domain={domain}/>
{
domain.entities.length > 0 &&
<>
<Divider orientation="left">{t`Entities`}</Divider>
<EntitiesList domain={domain}/>
</>
}
</Card>
</Badge.Ribbon>
</Space>
: <Empty
description={t`Although the domain exists in my database, it has been deleted from the WHOIS by its registrar.`}/>)
}
</Skeleton>
</Card>
<Skeleton loading={domain === null} active>
{
domain &&
(!domain.deleted ? <DomainResult domain={domain}/>
: <Empty
description={t`Although the domain exists in my database, it has been deleted from the WHOIS by its registrar.`}/>)
}
</Skeleton>
</Flex>
}

View File

@@ -6,28 +6,14 @@ import {regionNames} from "../../i18n";
import useBreakpoint from "../../hooks/useBreakpoint";
import {ColumnType} from "antd/es/table";
import punycode from "punycode/punycode";
import {getCountryCode} from "../../utils/functions/getCountryCode";
import {tldToEmoji} from "../../utils/functions/tldToEmoji";
const {Text, Paragraph} = Typography
type TldType = 'iTLD' | 'sTLD' | 'gTLD' | 'ccTLD'
type FiltersType = { type: TldType, contractTerminated?: boolean, specification13?: boolean }
const toEmoji = (tld: string) => {
if (tld.startsWith('xn--')) return '-'
return String.fromCodePoint(
...getCountryCode(tld)
.toUpperCase()
.split('')
.map((char) => 127397 + char.charCodeAt(0))
)
}
const getCountryCode = (tld: string): string => {
const exceptions = {uk: 'gb', su: 'ru', tp: 'tl'}
if (tld in exceptions) return exceptions[tld as keyof typeof exceptions]
return tld.toUpperCase()
}
function TldTable(filters: FiltersType) {
const sm = useBreakpoint('sm')
@@ -55,7 +41,7 @@ function TldTable(filters: FiltersType) {
return {
...rowData,
Flag: toEmoji(tld.tld),
Flag: tldToEmoji(tld.tld),
Country: countryName
}
case 'gTLD':
@@ -127,6 +113,7 @@ export default function TldPage() {
</Paragraph>
<Divider/>
<Collapse
accordion
size={sm ? 'small' : 'large'}
items={[
{

View File

@@ -2,10 +2,11 @@ import React, {useEffect, useState} from "react";
import {Card, Flex, Form, message, Skeleton} from "antd";
import {t} from "ttag";
import {Connector, getConnectors, postConnector} from "../../utils/api/connectors";
import {ConnectorForm} from "../../components/tracking/ConnectorForm";
import {ConnectorForm} from "../../components/tracking/connector/ConnectorForm";
import {AxiosError} from "axios";
import {ConnectorElement, ConnectorsList} from "../../components/tracking/ConnectorsList";
import {showErrorAPI} from "../../utils";
import {ConnectorElement, ConnectorsList} from "../../components/tracking/connector/ConnectorsList";
import {showErrorAPI} from "../../utils/functions/showErrorAPI";
export default function ConnectorsPage() {
const [form] = Form.useForm()

View File

@@ -1,12 +1,15 @@
import React, {useEffect, useState} from "react";
import {Card, Divider, Flex, Form, message} from "antd";
import {EventAction, getWatchlists, postWatchlist} from "../../utils/api";
import {Card, Divider, Flex, Form, message, Tag} from "antd";
import {EventAction, getWatchlists, postWatchlist, putWatchlist} from "../../utils/api";
import {AxiosError} from "axios";
import {t} from 'ttag'
import {WatchlistForm} from "../../components/tracking/WatchlistForm";
import {WatchlistsList} from "../../components/tracking/WatchlistsList";
import {WatchlistForm} from "../../components/tracking/watchlist/WatchlistForm";
import {WatchlistsList} from "../../components/tracking/watchlist/WatchlistsList";
import {Connector, getConnectors} from "../../utils/api/connectors";
import {showErrorAPI} from "../../utils";
import {showErrorAPI} from "../../utils/functions/showErrorAPI";
import {TrackedDomainTable} from "../../components/tracking/watchlist/TrackedDomainTable";
import {AimOutlined} from "@ant-design/icons";
export type Watchlist = {
@@ -14,6 +17,7 @@ export type Watchlist = {
token: string,
domains: { ldhName: string }[],
triggers?: { event: EventAction, action: string }[],
dsn?: string[]
connector?: {
id: string
provider: string
@@ -22,26 +26,42 @@ export type Watchlist = {
createdAt: string
}
type FormValuesType = {
name?: string
domains: string[],
triggers: string[]
connector?: string,
dsn?: string[]
}
const getRequestDataFromForm = (values: FormValuesType) => {
const domainsURI = values.domains.map(d => '/api/domains/' + d.toLowerCase())
let triggers = values.triggers.map(t => ({event: t, action: 'email'}))
if (values.dsn !== undefined) {
triggers = [...triggers, ...values.triggers.map(t => ({
event: t,
action: 'chat'
}))]
}
return {
name: values.name,
domains: domainsURI,
triggers,
connector: values.connector !== undefined ? ('/api/connectors/' + values.connector) : undefined,
dsn: values.dsn
}
}
export default function WatchlistPage() {
const [form] = Form.useForm()
const [messageApi, contextHolder] = message.useMessage()
const [watchlists, setWatchlists] = useState<Watchlist[] | null>()
const [connectors, setConnectors] = useState<(Connector & { id: string })[] | null>()
const [watchlists, setWatchlists] = useState<Watchlist[]>()
const [connectors, setConnectors] = useState<(Connector & { id: string })[]>()
const onCreateWatchlist = (values: {
name?: string
domains: string[],
emailTriggers: string[]
connector?: string
}) => {
const domainsURI = values.domains.map(d => '/api/domains/' + d)
postWatchlist({
name: values.name,
domains: domainsURI,
triggers: values.emailTriggers.map(t => ({event: t, action: 'email'})),
connector: values.connector !== undefined ? '/api/connectors/' + values.connector : undefined
}).then((w) => {
const onCreateWatchlist = (values: FormValuesType) => {
postWatchlist(getRequestDataFromForm(values)).then((w) => {
form.resetFields()
refreshWatchlists()
messageApi.success(t`Watchlist created !`)
@@ -50,6 +70,17 @@ export default function WatchlistPage() {
})
}
const onUpdateWatchlist = async (values: FormValuesType & { token: string }) => putWatchlist({
token: values.token,
...getRequestDataFromForm(values)
}
).then((w) => {
refreshWatchlists()
messageApi.success(t`Watchlist updated !`)
}).catch((e: AxiosError) => {
throw showErrorAPI(e, messageApi)
})
const refreshWatchlists = () => getWatchlists().then(w => {
setWatchlists(w['hydra:member'])
}).catch((e: AxiosError) => {
@@ -67,17 +98,27 @@ export default function WatchlistPage() {
}, [])
return <Flex gap="middle" align="center" justify="center" vertical>
<Card title={t`Create a Watchlist`} style={{width: '100%'}}>
{contextHolder}
{
connectors &&
<WatchlistForm form={form} onCreateWatchlist={onCreateWatchlist} connectors={connectors}/>
{contextHolder}
<Card loading={connectors === undefined} title={t`Create a Watchlist`} style={{width: '100%'}}>
{connectors &&
<WatchlistForm form={form} onFinish={onCreateWatchlist} connectors={connectors} isCreation={true}/>
}
</Card>
<Divider/>
{watchlists && watchlists.length > 0 &&
<WatchlistsList watchlists={watchlists} onDelete={refreshWatchlists}/>}
<Card title={
<>
<Tag icon={<AimOutlined/>} color="cyan-inverse"/>
{t`Tracked domain names`}
</>
}
style={{width: '100%'}}>
<TrackedDomainTable/>
</Card>
<Divider/>
{connectors && watchlists && watchlists.length > 0 &&
<WatchlistsList watchlists={watchlists} onDelete={refreshWatchlists}
connectors={connectors}
onUpdateWatchlist={onUpdateWatchlist}
/>}
</Flex>
}

View File

@@ -21,6 +21,7 @@ export type TriggerAction = 'email' | string
export interface Event {
action: EventAction
date: string
deleted: boolean
}
export interface Entity {
@@ -53,10 +54,12 @@ export interface Domain {
entity: Entity
events: Event[]
roles: string[]
deleted: boolean
}[]
nameservers: Nameserver[]
tld: Tld
deleted: boolean
updatedAt: string
}
export interface User {
@@ -64,11 +67,22 @@ export interface User {
roles: string[]
}
export interface Watchlist {
export interface WatchlistRequest {
name?: string
domains: string[],
triggers: { event: EventAction, action: TriggerAction }[],
connector?: string
dsn?: string[]
}
export interface Watchlist {
token: string
name?: string
domains: Domain[],
triggers: { event: EventAction, action: TriggerAction }[],
connector?: string
createdAt: string
dsn?: string[]
}
export interface InstanceConfig {
@@ -77,6 +91,16 @@ export interface InstanceConfig {
registerEnabled: boolean
}
export interface Statistics {
rdapQueries: number
alertSent: number
domainPurchased: number
domainPurchaseFailed: number
domainCount: {tld: string, domain: number}[]
domainCountTotal: number
domainTracked: number
}
export async function request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig): Promise<R> {
const axiosConfig: AxiosRequestConfig = {
...config,

View File

@@ -1,4 +1,4 @@
import {InstanceConfig, request, User} from "./index";
import {InstanceConfig, request, Statistics, User} from "./index";
export async function login(email: string, password: string): Promise<boolean> {
@@ -32,4 +32,11 @@ export async function getConfiguration(): Promise<InstanceConfig> {
url: 'config'
})
return response.data
}
export async function getStatistics(): Promise<Statistics> {
const response = await request<Statistics>({
url: 'stats'
})
return response.data
}

View File

@@ -1,4 +1,4 @@
import {Event, request, Watchlist} from "./index";
import {Domain, request, Watchlist, WatchlistRequest} from "./index";
export async function getWatchlists() {
const response = await request({
@@ -8,13 +8,13 @@ export async function getWatchlists() {
}
export async function getWatchlist(token: string) {
const response = await request<Watchlist & { token: string }>({
const response = await request<Watchlist>({
url: 'watchlists/' + token
})
return response.data
}
export async function postWatchlist(watchlist: Watchlist) {
export async function postWatchlist(watchlist: WatchlistRequest) {
const response = await request<{ token: string }>({
method: 'POST',
url: 'watchlists',
@@ -33,17 +33,21 @@ export async function deleteWatchlist(token: string): Promise<void> {
})
}
export async function patchWatchlist(domains: string[], triggers: Event[]) {
const response = await request<Watchlist>({
method: 'PATCH',
url: 'watchlists',
data: {
domains,
triggers
},
headers: {
"Content-Type": 'application/merge-patch+json'
}
export async function putWatchlist(watchlist: Partial<WatchlistRequest> & { token: string }) {
const response = await request<WatchlistRequest>({
method: 'PUT',
url: 'watchlists/' + watchlist.token,
data: watchlist,
})
return response.data
}
}
export async function getTrackedDomainList(params: { page: number, itemsPerPage: number }): Promise<any> {
const response = await request({
method: 'GET',
url: 'tracked',
params
})
return response.data
}

View File

@@ -0,0 +1,11 @@
import {EventAction} from "../api";
export const actionToColor = (a: EventAction) => a === 'registration' ? 'green' :
a === 'reregistration' ? 'cyan' :
a === 'expiration' ? 'red' :
a === 'deletion' ? 'magenta' :
a === 'transfer' ? 'orange' :
a === 'last changed' ? 'blue' :
a === 'registrar expiration' ? 'red' :
a === 'reinstantiation' ? 'purple' :
a === 'enum validation expiration' ? 'red' : 'default'

View File

@@ -0,0 +1,26 @@
import {EventAction} from "../api";
import {
ClockCircleOutlined,
DeleteOutlined,
LockOutlined,
ReloadOutlined,
ShareAltOutlined,
SignatureOutlined,
SyncOutlined,
UnlockOutlined
} from "@ant-design/icons";
import React from "react";
export const actionToIcon = (a: EventAction) => a === 'registration' ?
<SignatureOutlined style={{fontSize: '16px'}}/> : a === 'expiration' ?
<ClockCircleOutlined style={{fontSize: '16px'}}/> : a === 'transfer' ?
<ShareAltOutlined style={{fontSize: '16px'}}/> : a === 'last changed' ?
<SyncOutlined style={{fontSize: '16px'}}/> : a === 'deletion' ?
<DeleteOutlined style={{fontSize: '16px'}}/> : a === 'reregistration' ?
<ReloadOutlined style={{fontSize: '16px'}}/> : a === 'locked' ?
<LockOutlined style={{fontSize: '16px'}}/> : a === 'unlocked' ?
<UnlockOutlined style={{fontSize: '16px'}}/> : a === 'registrar expiration' ?
<ClockCircleOutlined
style={{fontSize: '16px'}}/> : a === 'enum validation expiration' ?
<ClockCircleOutlined style={{fontSize: '16px'}}/> : a === 'reinstantiation' ?
<ReloadOutlined style={{fontSize: '16px'}}/> : undefined

View File

@@ -0,0 +1,11 @@
import {Entity} from "../api";
import vCard from "vcf";
export const entityToName = (e: { entity: Entity }): string => {
if (e.entity.jCard.length === 0) return e.entity.handle
const jCard = vCard.fromJSON(e.entity.jCard)
let name = e.entity.handle
if (jCard.data.fn && !Array.isArray(jCard.data.fn) && jCard.data.fn.valueOf() !== '') name = jCard.data.fn.valueOf()
return name
}

View File

@@ -0,0 +1,5 @@
export const eppStatusCodeToColor = (s: string) =>
['active', 'ok'].includes(s) ? 'green' :
['pending delete', 'redemption period'].includes(s) ? 'red' :
s.startsWith('client') ? 'purple' :
s.startsWith('server') ? 'geekblue' : 'blue'

View File

@@ -0,0 +1,5 @@
export const getCountryCode = (tld: string): string => {
const exceptions = {uk: 'gb', su: 'ru', tp: 'tl'}
if (tld in exceptions) return exceptions[tld as keyof typeof exceptions]
return tld.toUpperCase()
}

View File

@@ -0,0 +1,117 @@
import {t} from "ttag";
/**
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
*/
export const rdapRoleTranslation = () => ({
registrant: t`Registrant`,
technical: t`Technical`,
administrative: t`Administrative`,
abuse: t`Abuse`,
billing: t`Billing`,
registrar: t`Registrar`,
reseller: t`Reseller`,
sponsor: t`Sponsor`,
proxy: t`Proxy`,
notifications: t`Notifications`,
noc: t`Noc`
})
/**
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
*/
export const rdapRoleDetailTranslation = () => ({
registrant: t`The entity object instance is the registrant of the registration. In some registries, this is known as a maintainer.`,
technical: t`The entity object instance is a technical contact for the registration.`,
administrative: t`The entity object instance is an administrative contact for the registration.`,
abuse: t`The entity object instance handles network abuse issues on behalf of the registrant of the registration.`,
billing: t`The entity object instance handles payment and billing issues on behalf of the registrant of the registration.`,
registrar: t`The entity object instance represents the authority responsible for the registration in the registry.`,
reseller: t`The entity object instance represents a third party through which the registration was conducted (i.e., not the registry or registrar).`,
sponsor: t`The entity object instance represents a domain policy sponsor, such as an ICANN-approved sponsor.`,
proxy: t`The entity object instance represents a proxy for another entity object, such as a registrant.`,
notifications: t`An entity object instance designated to receive notifications about association object instances.`,
noc: t`The entity object instance handles communications related to a network operations center (NOC).`
})
/**
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
*/
export const rdapEventNameTranslation = () => ({
registration: t`Registration`,
reregistration: t`Reregistration`,
'last changed': t`Changed`,
expiration: t`Expiration`,
deletion: t`Deletion`,
reinstantiation: t`Reinstantiation`,
transfer: t`Transfer`,
locked: t`Locked`,
unlocked: t`Unlocked`,
'registrar expiration': t`Registrar expiration`,
'enum validation expiration': t`ENUM validation expiration`
})
/**
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
*/
export const rdapEventDetailTranslation = () => ({
registration: t`The object instance was initially registered.`,
reregistration: t`The object instance was registered subsequently to initial registration.`,
'last changed': t`An action noting when the information in the object instance was last changed.`,
expiration: t`The object instance has been removed or will be removed at a predetermined date and time from the registry.`,
deletion: t`The object instance was removed from the registry at a point in time that was not predetermined.`,
reinstantiation: t`The object instance was reregistered after having been removed from the registry.`,
transfer: t`The object instance was transferred from one registrar to another.`,
locked: t`The object instance was locked.`,
unlocked: t`The object instance was unlocked.`,
'registrar expiration': t`An action noting the expiration date of the object in the registrar system.`,
'enum validation expiration': t`Association of phone number represented by this ENUM domain to registrant has expired or will expire at a predetermined date and time.`
})
/**
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
* @see https://www.icann.org/resources/pages/epp-status-codes-2014-06-16-en
*/
export const rdapStatusCodeDetailTranslation = () => ({
'validated': t`Signifies that the data of the object instance has been found to be accurate.`,
'renew prohibited': t`Renewal or reregistration of the object instance is forbidden.`,
'update prohibited': t`Updates to the object instance are forbidden.`,
'transfer prohibited': t`Transfers of the registration from one registrar to another are forbidden.`,
'delete prohibited': t`Deletion of the registration of the object instance is forbidden.`,
'proxy': t`The registration of the object instance has been performed by a third party.`,
'private': t`The information of the object instance is not designated for public consumption.`,
'removed': t`Some of the information of the object instance has not been made available and has been removed.`,
'obscured': t`Some of the information of the object instance has been altered for the purposes of not readily revealing the actual information of the object instance.`,
'associated': t`The object instance is associated with other object instances in the registry.`,
'locked': t`Changes to the object instance cannot be made, including the association of other object instances.`,
'active': t`This is the standard status for a domain, meaning it has no pending operations or prohibitions.`,
'inactive': t`This status code indicates that delegation information (name servers) has not been associated with your domain. Your domain is not activated in the DNS and will not resolve.`,
'pending create': t`This status code indicates that a request to create your domain has been received and is being processed.`,
'pending renew': t`This status code indicates that a request to renew your domain has been received and is being processed.`,
'pending transfer': t`This status code indicates that a request to transfer your domain to a new registrar has been received and is being processed.`,
'pending update': t`This status code indicates that a request to update your domain has been received and is being processed.`,
'pending delete': t`This status code may be mixed with redemptionPeriod or pendingRestore. In such case, depending on the status (i.e. redemptionPeriod or pendingRestore) set in the domain name, the corresponding description presented above applies. If this status is not combined with the redemptionPeriod or pendingRestore status, the pendingDelete status code indicates that your domain has been in redemptionPeriod status for 30 days and you have not restored it within that 30-day period. Your domain will remain in this status for several days, after which time your domain will be purged and dropped from the registry database. Once deletion occurs, the domain is available for re-registration in accordance with the registry's policies.`,
'add period': t`This grace period is provided after the initial registration of a domain name. If the registrar deletes the domain name during this period, the registry may provide credit to the registrar for the cost of the registration.`,
'auto renew period': t`This grace period is provided after a domain name registration period expires and is extended (renewed) automatically by the registry. If the registrar deletes the domain name during this period, the registry provides a credit to the registrar for the cost of the renewal.`,
'ok': t`This is the standard status for a domain, meaning it has no pending operations or prohibitions.`,
'client delete prohibited': t`This status code tells your domain's registry to reject requests to delete the domain.`,
'client hold': t`This status code tells your domain's registry to not activate your domain in the DNS and as a consequence, it will not resolve. It is an uncommon status that is usually enacted during legal disputes, non-payment, or when your domain is subject to deletion.`,
'client renew prohibited': t`This status code tells your domain's registry to reject requests to renew your domain. It is an uncommon status that is usually enacted during legal disputes or when your domain is subject to deletion.`,
'client transfer prohibited': t`This status code tells your domain's registry to reject requests to transfer the domain from your current registrar to another.`,
'client update prohibited': t`This status code tells your domain's registry to reject requests to update the domain.`,
'pending restore': t`This status code indicates that your registrar has asked the registry to restore your domain that was in redemptionPeriod status. Your registry will hold the domain in this status while waiting for your registrar to provide required restoration documentation. If your registrar fails to provide documentation to the registry operator within a set time period to confirm the restoration request, the domain will revert to redemptionPeriod status.`,
'redemption period': t`This status code indicates that your registrar has asked the registry to delete your domain. Your domain will be held in this status for 30 days. After five calendar days following the end of the redemptionPeriod, your domain is purged from the registry database and becomes available for registration.`,
'renew period': t`This grace period is provided after a domain name registration period is explicitly extended (renewed) by the registrar. If the registrar deletes the domain name during this period, the registry provides a credit to the registrar for the cost of the renewal.`,
'server delete prohibited': t`This status code prevents your domain from being deleted. It is an uncommon status that is usually enacted during legal disputes, at your request, or when a redemptionPeriod status is in place.`,
'server renew prohibited': t`This status code indicates your domain's Registry Operator will not allow your registrar to renew your domain. It is an uncommon status that is usually enacted during legal disputes or when your domain is subject to deletion.`,
'server transfer prohibited': t`This status code prevents your domain from being transferred from your current registrar to another. It is an uncommon status that is usually enacted during legal or other disputes, at your request, or when a redemptionPeriod status is in place.`,
'server update prohibited': t`This status code locks your domain preventing it from being updated. It is an uncommon status that is usually enacted during legal disputes, at your request, or when a redemptionPeriod status is in place.`,
'server hold': t`This status code is set by your domain's Registry Operator. Your domain is not activated in the DNS.`,
'transfer period': t`This grace period is provided after the successful transfer of a domain name from one registrar to another. If the new registrar deletes the domain name during this period, the registry provides a credit to the registrar for the cost of the transfer.`,
'administrative': t`The object instance has been allocated administratively (i.e., not for use by the recipient in their own right in operational networks).`,
'reserved': t`The object instance has been allocated to an IANA special-purpose address registry.`,
})

View File

@@ -0,0 +1,24 @@
import {Avatar} from "antd";
import {
BankOutlined,
DollarOutlined,
IdcardOutlined,
SignatureOutlined,
ToolOutlined,
UserOutlined
} from "@ant-design/icons";
import React from "react";
import {rolesToColor} from "./rolesToColor";
export const roleToAvatar = (e: { roles: string[] }) => <Avatar style={{backgroundColor: rolesToColor(e.roles)}}
icon={e.roles.includes('registrant') ?
<SignatureOutlined/> : e.roles.includes('registrar') ?
<BankOutlined/> :
e.roles.includes('technical') ?
<ToolOutlined/> :
e.roles.includes('administrative') ?
<IdcardOutlined/> :
e.roles.includes('billing') ?
<DollarOutlined/> :
<UserOutlined/>}/>

View File

@@ -0,0 +1,6 @@
export const rolesToColor = (roles: string[]) => roles.includes('registrant') ? 'green' :
roles.includes('technical') ? 'orange' :
roles.includes('administrative') ? 'blue' :
roles.includes('registrar') ? 'purple' :
roles.includes('sponsor') ? 'magenta' :
roles.includes('billing') ? 'cyan' : 'default'

View File

@@ -1,5 +1,5 @@
import {MessageInstance, MessageType} from "antd/lib/message/interface";
import {AxiosError, AxiosResponse} from "axios";
import {MessageInstance, MessageType} from "antd/lib/message/interface";
import {t} from "ttag";
export function showErrorAPI(e: AxiosError, messageApi: MessageInstance): MessageType | undefined {

View File

@@ -0,0 +1,11 @@
import {Domain} from "../api";
export const sortDomainEntities = (domain: Domain) => domain.entities
.filter(e => !e.deleted)
.sort((e1, e2) => {
const p = (r: string[]) => r.includes('registrant') ? 5 :
r.includes('administrative') ? 4 :
r.includes('billing') ? 3 :
r.includes('registrar') ? 2 : 1
return p(e2.roles) - p(e1.roles)
})

View File

@@ -0,0 +1,12 @@
import {getCountryCode} from "./getCountryCode";
export const tldToEmoji = (tld: string) => {
if (tld.startsWith('xn--')) return '-'
return String.fromCodePoint(
...getCountryCode(tld)
.toUpperCase()
.split('')
.map((char) => 127397 + char.charCodeAt(0))
)
}

View File

@@ -7,7 +7,7 @@ export const helpGetTokenLink = (provider?: string) => {
switch (provider) {
case ConnectorProvider.OVH:
return <Typography.Link target='_blank'
href="https://api.ovh.com/createToken/index.cgi?GET=/order/cart&GET=/order/cart/*&POST=/order/cart&POST=/order/cart/*&DELETE=/order/cart/*">
href="https://api.ovh.com/createToken/?GET=/order/cart&GET=/order/cart/*&POST=/order/cart&POST=/order/cart/*&DELETE=/order/cart/*&GET=/domain/extensions">
{t`Retrieve a set of tokens from your customer account on the Provider's website`}
</Typography.Link>