mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-17 17:55:42 +00:00
Merge branch 'feat/anonym-domain-lookup' into develop
This commit is contained in:
commit
43b0253eab
1
.env
1
.env
@ -86,6 +86,7 @@ LIMITED_FEATURES=false
|
|||||||
LIMIT_MAX_WATCHLIST=0
|
LIMIT_MAX_WATCHLIST=0
|
||||||
LIMIT_MAX_WATCHLIST_DOMAINS=0
|
LIMIT_MAX_WATCHLIST_DOMAINS=0
|
||||||
LIMIT_MAX_WATCHLIST_WEBHOOKS=0
|
LIMIT_MAX_WATCHLIST_WEBHOOKS=0
|
||||||
|
PUBLIC_RDAP_LOOKUP_ENABLE=false
|
||||||
|
|
||||||
# STATISTICS
|
# STATISTICS
|
||||||
INFLUXDB_ENABLED=false
|
INFLUXDB_ENABLED=false
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {Button, ConfigProvider, Drawer, Flex, Layout, theme, Typography} from 'antd'
|
import {Button, ConfigProvider, Drawer, Flex, Layout, message, theme, Typography} from 'antd'
|
||||||
import {Link, Navigate, Route, Routes, useLocation, useNavigate} from 'react-router-dom'
|
import {Link, Navigate, Route, Routes, useLocation, useNavigate} from 'react-router-dom'
|
||||||
import TextPage from './pages/TextPage'
|
import TextPage from './pages/TextPage'
|
||||||
import DomainSearchPage from './pages/search/DomainSearchPage'
|
import DomainSearchPage from './pages/search/DomainSearchPage'
|
||||||
@ -9,8 +9,8 @@ import WatchlistPage from './pages/tracking/WatchlistPage'
|
|||||||
import UserPage from './pages/UserPage'
|
import UserPage from './pages/UserPage'
|
||||||
import type {PropsWithChildren} from 'react'
|
import type {PropsWithChildren} from 'react'
|
||||||
import React, { useCallback, useEffect, useMemo, useState} from 'react'
|
import React, { useCallback, useEffect, useMemo, useState} from 'react'
|
||||||
import {getUser} from './utils/api'
|
import {getConfiguration, getUser, type InstanceConfig} from './utils/api'
|
||||||
import LoginPage, {AuthenticatedContext} from './pages/LoginPage'
|
import LoginPage from './pages/LoginPage'
|
||||||
import ConnectorPage from './pages/tracking/ConnectorPage'
|
import ConnectorPage from './pages/tracking/ConnectorPage'
|
||||||
import NotFoundPage from './pages/NotFoundPage'
|
import NotFoundPage from './pages/NotFoundPage'
|
||||||
import useBreakpoint from './hooks/useBreakpoint'
|
import useBreakpoint from './hooks/useBreakpoint'
|
||||||
@ -19,12 +19,14 @@ import {jt, t} from 'ttag'
|
|||||||
import {MenuOutlined} from '@ant-design/icons'
|
import {MenuOutlined} from '@ant-design/icons'
|
||||||
import TrackedDomainPage from './pages/tracking/TrackedDomainPage'
|
import TrackedDomainPage from './pages/tracking/TrackedDomainPage'
|
||||||
import IcannRegistrarPage from "./pages/infrastructure/IcannRegistrarPage"
|
import IcannRegistrarPage from "./pages/infrastructure/IcannRegistrarPage"
|
||||||
|
import type {AuthContextType} from "./contexts"
|
||||||
|
import {AuthenticatedContext, ConfigurationContext} from "./contexts"
|
||||||
|
|
||||||
const PROJECT_LINK = 'https://github.com/maelgangloff/domain-watchdog'
|
const PROJECT_LINK = 'https://github.com/maelgangloff/domain-watchdog'
|
||||||
const LICENSE_LINK = 'https://www.gnu.org/licenses/agpl-3.0.txt'
|
const LICENSE_LINK = 'https://www.gnu.org/licenses/agpl-3.0.txt'
|
||||||
|
|
||||||
const ProjectLink = <Typography.Link key="projectLink" target='_blank' href={PROJECT_LINK}>Domain Watchdog</Typography.Link>
|
const ProjectLink = <Typography.Link key="projectLink" target='_blank' href={PROJECT_LINK}>Domain Watchdog</Typography.Link>
|
||||||
const LicenseLink = <Typography.Link key="licenceLink" target='_blank' href={LICENSE_LINK}>AGPL-3.0-or-later</Typography.Link>
|
const LicenseLink = <Typography.Link key="licenceLink" target='_blank' rel='license' href={LICENSE_LINK}>AGPL-3.0-or-later</Typography.Link>
|
||||||
|
|
||||||
function SiderWrapper(props: PropsWithChildren<{sidebarCollapsed: boolean, setSidebarCollapsed: (collapsed: boolean) => void}>): React.ReactElement {
|
function SiderWrapper(props: PropsWithChildren<{sidebarCollapsed: boolean, setSidebarCollapsed: (collapsed: boolean) => void}>): React.ReactElement {
|
||||||
const {sidebarCollapsed, setSidebarCollapsed, children} = props
|
const {sidebarCollapsed, setSidebarCollapsed, children} = props
|
||||||
@ -68,18 +70,22 @@ export default function App(): React.ReactElement {
|
|||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||||
|
|
||||||
const authenticated = useCallback((authenticated: boolean) => {
|
const [configuration, setConfiguration] = useState<InstanceConfig | undefined>(undefined)
|
||||||
setIsAuthenticated(authenticated)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const contextValue = useMemo(() => ({
|
|
||||||
authenticated,
|
|
||||||
setIsAuthenticated
|
|
||||||
}), [authenticated, setIsAuthenticated])
|
|
||||||
|
|
||||||
const [darkMode, setDarkMode] = useState(false)
|
const [darkMode, setDarkMode] = useState(false)
|
||||||
|
|
||||||
const windowQuery = window.matchMedia('(prefers-color-scheme:dark)')
|
const windowQuery = window.matchMedia('(prefers-color-scheme:dark)')
|
||||||
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
|
|
||||||
|
|
||||||
|
const authContextValue: AuthContextType = useMemo(() => ({
|
||||||
|
isAuthenticated,
|
||||||
|
setIsAuthenticated
|
||||||
|
}), [isAuthenticated])
|
||||||
|
|
||||||
|
const configContextValue = useMemo(() => ({
|
||||||
|
configuration,
|
||||||
|
}), [configuration])
|
||||||
|
|
||||||
const darkModeChange = useCallback((event: MediaQueryListEvent) => {
|
const darkModeChange = useCallback((event: MediaQueryListEvent) => {
|
||||||
setDarkMode(event.matches)
|
setDarkMode(event.matches)
|
||||||
}, [])
|
}, [])
|
||||||
@ -93,14 +99,19 @@ export default function App(): React.ReactElement {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDarkMode(windowQuery.matches)
|
setDarkMode(windowQuery.matches)
|
||||||
getUser().then(() => {
|
getConfiguration().then(configuration => {
|
||||||
setIsAuthenticated(true)
|
setConfiguration(configuration)
|
||||||
if (location.pathname === '/login') navigate('/home')
|
|
||||||
}).catch(() => {
|
getUser().then(() => {
|
||||||
setIsAuthenticated(false)
|
setIsAuthenticated(true)
|
||||||
const pathname = location.pathname
|
if (location.pathname === '/login') navigate('/home')
|
||||||
if (!['/login', '/tos', '/faq', '/privacy'].includes(pathname)) navigate('/home')
|
}).catch(() => {
|
||||||
})
|
setIsAuthenticated(false)
|
||||||
|
const pathname = location.pathname
|
||||||
|
if(configuration.publicRdapLookupEnabled) return navigate('/search/domain')
|
||||||
|
if (!['/login', '/tos', '/faq', '/privacy'].includes(pathname)) return navigate('/home')
|
||||||
|
})
|
||||||
|
}).catch(() => messageApi.error(t`Unable to contact the server, please reload the page.`))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -109,10 +120,11 @@ export default function App(): React.ReactElement {
|
|||||||
algorithm: darkMode ? theme.darkAlgorithm : undefined
|
algorithm: darkMode ? theme.darkAlgorithm : undefined
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AuthenticatedContext.Provider value={contextValue}>
|
<ConfigurationContext.Provider value={configContextValue}>
|
||||||
|
<AuthenticatedContext.Provider value={authContextValue}>
|
||||||
<Layout hasSider style={{minHeight: '100vh'}}>
|
<Layout hasSider style={{minHeight: '100vh'}}>
|
||||||
<SiderWrapper sidebarCollapsed={sidebarCollapsed} setSidebarCollapsed={setSidebarCollapsed}>
|
<SiderWrapper sidebarCollapsed={sidebarCollapsed} setSidebarCollapsed={setSidebarCollapsed}>
|
||||||
<Sider isAuthenticated={isAuthenticated}/>
|
<Sider />
|
||||||
</SiderWrapper>
|
</SiderWrapper>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Layout.Header style={{padding: 0}}>
|
<Layout.Header style={{padding: 0}}>
|
||||||
@ -128,8 +140,9 @@ export default function App(): React.ReactElement {
|
|||||||
minHeight: 360
|
minHeight: 360
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{contextHolder}
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/' element={<Navigate to='/home'/>}/>
|
<Route path='/' element={<Navigate to={configuration?.publicRdapLookupEnabled ? '/search/domain' : '/home'}/>}/>
|
||||||
<Route path='/home' element={<TextPage resource='home.md'/>}/>
|
<Route path='/home' element={<TextPage resource='home.md'/>}/>
|
||||||
|
|
||||||
<Route path='/search/domain' element={<DomainSearchPage/>}/>
|
<Route path='/search/domain' element={<DomainSearchPage/>}/>
|
||||||
@ -158,8 +171,8 @@ export default function App(): React.ReactElement {
|
|||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
<Layout.Footer style={{textAlign: 'center'}}>
|
<Layout.Footer style={{textAlign: 'center'}}>
|
||||||
<Flex gap='middle' wrap justify='center'>
|
<Flex gap='middle' wrap justify='center'>
|
||||||
<Link to='/tos'><Button type='text'>{t`TOS`}</Button></Link>
|
<Link to='/tos' rel='terms-of-service'><Button type='text'>{t`TOS`}</Button></Link>
|
||||||
<Link to='/privacy'><Button type='text'>{t`Privacy Policy`}</Button></Link>
|
<Link to='/privacy' rel='privacy-policy'><Button type='text'>{t`Privacy Policy`}</Button></Link>
|
||||||
<Link to='/faq'><Button type='text'>{t`FAQ`}</Button></Link>
|
<Link to='/faq'><Button type='text'>{t`FAQ`}</Button></Link>
|
||||||
<Typography.Link
|
<Typography.Link
|
||||||
target='_blank'
|
target='_blank'
|
||||||
@ -193,6 +206,7 @@ export default function App(): React.ReactElement {
|
|||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
</AuthenticatedContext.Provider>
|
</AuthenticatedContext.Provider>
|
||||||
|
</ConfigurationContext.Provider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,10 @@ import {Button, Flex, Form, Input, message} from 'antd'
|
|||||||
import {t} from 'ttag'
|
import {t} from 'ttag'
|
||||||
import React, {useContext, useEffect, useState} from 'react'
|
import React, {useContext, useEffect, useState} from 'react'
|
||||||
import {getUser, login} from '../utils/api'
|
import {getUser, login} from '../utils/api'
|
||||||
import {AuthenticatedContext} from '../pages/LoginPage'
|
|
||||||
import {useNavigate} from 'react-router-dom'
|
import {useNavigate} from 'react-router-dom'
|
||||||
|
|
||||||
import {showErrorAPI} from '../utils/functions/showErrorAPI'
|
import {showErrorAPI} from '../utils/functions/showErrorAPI'
|
||||||
|
import {AuthenticatedContext} from "../contexts"
|
||||||
|
|
||||||
interface FieldType {
|
interface FieldType {
|
||||||
username: string
|
username: string
|
||||||
@ -66,9 +66,9 @@ export function LoginForm({ssoLogin}: { ssoLogin?: boolean }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Flex wrap justify="center" gap="middle">
|
<Flex wrap justify="center" gap="middle">
|
||||||
<Button type='primary' htmlType='submit'>
|
<Button type='primary' htmlType='submit'>
|
||||||
{t`Submit`}
|
{t`Submit`}
|
||||||
</Button>
|
</Button>
|
||||||
{ssoLogin &&
|
{ssoLogin &&
|
||||||
<Button type='dashed' htmlType='button' href='/login/oauth'>
|
<Button type='dashed' htmlType='button' href='/login/oauth'>
|
||||||
{t`Log in with SSO`}
|
{t`Log in with SSO`}
|
||||||
|
|||||||
@ -17,10 +17,14 @@ import {
|
|||||||
UserOutlined
|
UserOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import {Menu} from 'antd'
|
import {Menu} from 'antd'
|
||||||
import React from 'react'
|
import React, {useContext} from 'react'
|
||||||
import {useLocation, useNavigate} from 'react-router-dom'
|
import {useLocation, useNavigate} from 'react-router-dom'
|
||||||
|
import {AuthenticatedContext, ConfigurationContext} from "../contexts"
|
||||||
|
|
||||||
|
export function Sider() {
|
||||||
|
const {isAuthenticated} = useContext(AuthenticatedContext)
|
||||||
|
const {configuration} = useContext(ConfigurationContext)
|
||||||
|
|
||||||
export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
@ -42,7 +46,7 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
|
|||||||
icon: <CompassOutlined/>,
|
icon: <CompassOutlined/>,
|
||||||
label: t`Domain`,
|
label: t`Domain`,
|
||||||
title: t`Domain Finder`,
|
title: t`Domain Finder`,
|
||||||
disabled: !isAuthenticated,
|
disabled: !configuration?.publicRdapLookupEnabled && !isAuthenticated,
|
||||||
onClick: () => navigate('/search/domain')
|
onClick: () => navigate('/search/domain')
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
|
|||||||
24
assets/contexts/index.ts
Normal file
24
assets/contexts/index.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type React from 'react'
|
||||||
|
import {createContext} from 'react'
|
||||||
|
import type {InstanceConfig} from "../utils/api"
|
||||||
|
|
||||||
|
|
||||||
|
export type ConfigurationContextType = {
|
||||||
|
configuration: InstanceConfig | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConfigurationContext = createContext<ConfigurationContextType>({
|
||||||
|
configuration: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export type AuthContextType = {
|
||||||
|
isAuthenticated: boolean
|
||||||
|
setIsAuthenticated: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthenticatedContext = createContext<AuthContextType>({
|
||||||
|
isAuthenticated: false,
|
||||||
|
setIsAuthenticated: () => {
|
||||||
|
},
|
||||||
|
})
|
||||||
@ -1,28 +1,16 @@
|
|||||||
import React, {createContext, useEffect, useState} from 'react'
|
import React, { useContext, useEffect, useState} from 'react'
|
||||||
import {Button, Card} from 'antd'
|
import {Button, Card} from 'antd'
|
||||||
import {t} from 'ttag'
|
import {t} from 'ttag'
|
||||||
import TextPage from './TextPage'
|
import TextPage from './TextPage'
|
||||||
import {LoginForm} from '../components/LoginForm'
|
import {LoginForm} from '../components/LoginForm'
|
||||||
import type { InstanceConfig} from '../utils/api'
|
|
||||||
import {getConfiguration} from '../utils/api'
|
|
||||||
import {RegisterForm} from '../components/RegisterForm'
|
import {RegisterForm} from '../components/RegisterForm'
|
||||||
import useBreakpoint from "../hooks/useBreakpoint"
|
import useBreakpoint from "../hooks/useBreakpoint"
|
||||||
|
import {ConfigurationContext} from "../contexts"
|
||||||
export const AuthenticatedContext = createContext<
|
|
||||||
{
|
|
||||||
authenticated: (authenticated: boolean) => void
|
|
||||||
setIsAuthenticated: React.Dispatch<React.SetStateAction<boolean>>
|
|
||||||
}
|
|
||||||
>({
|
|
||||||
authenticated: () => {
|
|
||||||
},
|
|
||||||
setIsAuthenticated: () => {
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [wantRegister, setWantRegister] = useState<boolean>(false)
|
const [wantRegister, setWantRegister] = useState<boolean>(false)
|
||||||
const [configuration, setConfiguration] = useState<InstanceConfig>()
|
const { configuration } = useContext(ConfigurationContext)
|
||||||
|
|
||||||
const md = useBreakpoint('md')
|
const md = useBreakpoint('md')
|
||||||
|
|
||||||
const toggleWantRegister = () => {
|
const toggleWantRegister = () => {
|
||||||
@ -30,14 +18,10 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getConfiguration().then((configuration) => {
|
if(!configuration?.registerEnabled && configuration?.ssoLogin && configuration?.ssoAutoRedirect) {
|
||||||
if(!configuration.registerEnabled && configuration.ssoLogin && configuration.ssoAutoRedirect) {
|
window.location.href = '/login/oauth'
|
||||||
window.location.href = '/login/oauth'
|
}
|
||||||
return
|
}, [configuration])
|
||||||
}
|
|
||||||
setConfiguration(configuration)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const grid = [
|
const grid = [
|
||||||
<Card.Grid key="form" style={{width: md ? '100%' : '50%', textAlign: 'center'}} hoverable={false}>
|
<Card.Grid key="form" style={{width: md ? '100%' : '50%', textAlign: 'center'}} hoverable={false}>
|
||||||
|
|||||||
@ -1,19 +1,18 @@
|
|||||||
import React, {useEffect, useState} from 'react'
|
import React, {useContext, useEffect, useState} from 'react'
|
||||||
import type { FormProps} from 'antd'
|
import type {FormProps} from 'antd'
|
||||||
import {FloatButton} from 'antd'
|
import {Empty, Flex, FloatButton, message, Skeleton} from 'antd'
|
||||||
import {Empty, Flex, message, Skeleton} from 'antd'
|
|
||||||
import type {Domain, Watchlist} from '../../utils/api'
|
import type {Domain, Watchlist} from '../../utils/api'
|
||||||
import {addDomainToWatchlist} from '../../utils/api'
|
import {addDomainToWatchlist, getDomain} from '../../utils/api'
|
||||||
import {getDomain} from '../../utils/api'
|
|
||||||
import type {AxiosError} from 'axios'
|
import type {AxiosError} from 'axios'
|
||||||
import {t} from 'ttag'
|
import {t} from 'ttag'
|
||||||
import type { FieldType} from '../../components/search/DomainSearchBar'
|
import type {FieldType} from '../../components/search/DomainSearchBar'
|
||||||
import {DomainSearchBar} from '../../components/search/DomainSearchBar'
|
import {DomainSearchBar} from '../../components/search/DomainSearchBar'
|
||||||
import {DomainResult} from '../../components/search/DomainResult'
|
import {DomainResult} from '../../components/search/DomainResult'
|
||||||
import {showErrorAPI} from '../../utils/functions/showErrorAPI'
|
import {showErrorAPI} from '../../utils/functions/showErrorAPI'
|
||||||
import {useNavigate, useParams} from 'react-router-dom'
|
import {useNavigate, useParams} from 'react-router-dom'
|
||||||
import {PlusOutlined} from '@ant-design/icons'
|
import {PlusOutlined} from '@ant-design/icons'
|
||||||
import WatchlistSelectionModal from '../../components/tracking/watchlist/WatchlistSelectionModal'
|
import WatchlistSelectionModal from '../../components/tracking/watchlist/WatchlistSelectionModal'
|
||||||
|
import {AuthenticatedContext} from "../../contexts"
|
||||||
|
|
||||||
export default function DomainSearchPage() {
|
export default function DomainSearchPage() {
|
||||||
const {query} = useParams()
|
const {query} = useParams()
|
||||||
@ -21,6 +20,8 @@ export default function DomainSearchPage() {
|
|||||||
const domainLdhName = domain?.ldhName
|
const domainLdhName = domain?.ldhName
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [addToWatchlistModal, setAddToWatchlistModal] = useState(false)
|
const [addToWatchlistModal, setAddToWatchlistModal] = useState(false)
|
||||||
|
const {isAuthenticated} = useContext(AuthenticatedContext)
|
||||||
|
|
||||||
|
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -72,7 +73,7 @@ export default function DomainSearchPage() {
|
|||||||
}
|
}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
</Flex>
|
</Flex>
|
||||||
{domain
|
{domain && isAuthenticated
|
||||||
&& <FloatButton
|
&& <FloatButton
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
@ -94,7 +95,7 @@ export default function DomainSearchPage() {
|
|||||||
onClose: () => setAddToWatchlistModal(false),
|
onClose: () => setAddToWatchlistModal(false),
|
||||||
cancelText: t`Cancel`,
|
cancelText: t`Cancel`,
|
||||||
okText: t`Add`
|
okText: t`Add`
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -109,6 +109,7 @@ export interface InstanceConfig {
|
|||||||
ssoLogin: boolean
|
ssoLogin: boolean
|
||||||
limtedFeatures: boolean
|
limtedFeatures: boolean
|
||||||
registerEnabled: boolean
|
registerEnabled: boolean
|
||||||
|
publicRdapLookupEnabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Statistics {
|
export interface Statistics {
|
||||||
|
|||||||
@ -22,5 +22,10 @@ framework:
|
|||||||
|
|
||||||
user_rdap_requests:
|
user_rdap_requests:
|
||||||
policy: sliding_window
|
policy: sliding_window
|
||||||
limit: 10
|
limit: 60
|
||||||
|
interval: '1 hour'
|
||||||
|
|
||||||
|
public_rdap_requests:
|
||||||
|
policy: sliding_window
|
||||||
|
limit: 30
|
||||||
interval: '1 hour'
|
interval: '1 hour'
|
||||||
|
|||||||
@ -60,6 +60,7 @@ security:
|
|||||||
- { path: ^/api$, roles: PUBLIC_ACCESS }
|
- { path: ^/api$, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/register$, roles: PUBLIC_ACCESS }
|
- { path: ^/api/register$, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/api/domains/*, roles: CAN_RDAP_LOOKUP }
|
||||||
- { path: "^/api/watchlists/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/calendar$", roles: PUBLIC_ACCESS }
|
- { path: "^/api/watchlists/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/calendar$", roles: PUBLIC_ACCESS }
|
||||||
- { path: "^/api/watchlists/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/rss", roles: PUBLIC_ACCESS }
|
- { path: "^/api/watchlists/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/rss", roles: PUBLIC_ACCESS }
|
||||||
- { path: "^/api/config$", roles: PUBLIC_ACCESS }
|
- { path: "^/api/config$", roles: PUBLIC_ACCESS }
|
||||||
|
|||||||
@ -7,13 +7,27 @@ parameters:
|
|||||||
custom_rdap_servers_file: '%kernel.project_dir%/config/app/custom_rdap_servers.yaml'
|
custom_rdap_servers_file: '%kernel.project_dir%/config/app/custom_rdap_servers.yaml'
|
||||||
|
|
||||||
mailer_sender_email: '%env(string:MAILER_SENDER_EMAIL)%'
|
mailer_sender_email: '%env(string:MAILER_SENDER_EMAIL)%'
|
||||||
|
|
||||||
mailer_sender_name: '%env(string:MAILER_SENDER_NAME)%'
|
mailer_sender_name: '%env(string:MAILER_SENDER_NAME)%'
|
||||||
|
env(MAILER_SENDER_NAME): Domain Watchdog
|
||||||
|
|
||||||
oauth_enabled: '%env(OAUTH_CLIENT_ID)%'
|
oauth_enabled: '%env(OAUTH_CLIENT_ID)%'
|
||||||
|
|
||||||
sso_auto_redirect: '%env(bool:SSO_AUTO_REDIRECT)%'
|
sso_auto_redirect: '%env(bool:SSO_AUTO_REDIRECT)%'
|
||||||
|
env(SSO_AUTO_REDIRECT): false
|
||||||
|
|
||||||
registration_enabled: '%env(bool:REGISTRATION_ENABLED)%'
|
registration_enabled: '%env(bool:REGISTRATION_ENABLED)%'
|
||||||
|
env(REGISTRATION_ENABLED): true
|
||||||
|
|
||||||
registration_verify_email: '%env(bool:REGISTRATION_VERIFY_EMAIL)%'
|
registration_verify_email: '%env(bool:REGISTRATION_VERIFY_EMAIL)%'
|
||||||
|
env(REGISTRATION_VERIFY_EMAIL): false
|
||||||
|
|
||||||
|
public_rdap_lookup_enabled: '%env(bool:PUBLIC_RDAP_LOOKUP_ENABLED)%'
|
||||||
|
env(PUBLIC_RDAP_LOOKUP_ENABLED): false
|
||||||
|
|
||||||
limited_features: '%env(bool:LIMITED_FEATURES)%'
|
limited_features: '%env(bool:LIMITED_FEATURES)%'
|
||||||
|
env(LIMITED_FEATURES): false
|
||||||
|
|
||||||
limit_max_watchlist: '%env(int:LIMIT_MAX_WATCHLIST)%'
|
limit_max_watchlist: '%env(int:LIMIT_MAX_WATCHLIST)%'
|
||||||
limit_max_watchlist_domains: '%env(int:LIMIT_MAX_WATCHLIST_DOMAINS)%'
|
limit_max_watchlist_domains: '%env(int:LIMIT_MAX_WATCHLIST_DOMAINS)%'
|
||||||
limit_max_watchlist_webhooks: '%env(int:LIMIT_MAX_WATCHLIST_WEBHOOKS)%'
|
limit_max_watchlist_webhooks: '%env(int:LIMIT_MAX_WATCHLIST_WEBHOOKS)%'
|
||||||
@ -21,6 +35,8 @@ parameters:
|
|||||||
outgoing_ip: '%env(string:OUTGOING_IP)%'
|
outgoing_ip: '%env(string:OUTGOING_IP)%'
|
||||||
|
|
||||||
influxdb_enabled: '%env(bool:INFLUXDB_ENABLED)%'
|
influxdb_enabled: '%env(bool:INFLUXDB_ENABLED)%'
|
||||||
|
env(INFLUXDB_ENABLED): false
|
||||||
|
|
||||||
influxdb_url: '%env(string:INFLUXDB_URL)%'
|
influxdb_url: '%env(string:INFLUXDB_URL)%'
|
||||||
influxdb_token: '%env(string:INFLUXDB_TOKEN)%'
|
influxdb_token: '%env(string:INFLUXDB_TOKEN)%'
|
||||||
influxdb_bucket: '%env(string:INFLUXDB_BUCKET)%'
|
influxdb_bucket: '%env(string:INFLUXDB_BUCKET)%'
|
||||||
|
|||||||
@ -9,31 +9,32 @@ import {LinkCard} from '@astrojs/starlight/components';
|
|||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|--------------------------------|----------------------------------------------|:---------------------------:|
|
|--------------------------------|------------------------------------------------|:---------------------------:|
|
||||||
| `DATABASE_URL` | Please check Symfony config | |
|
| `DATABASE_URL` | Please check Symfony config | |
|
||||||
| `OUTGOING_IP` | Outgoing IPv4, needed for some providers | |
|
| `OUTGOING_IP` | Outgoing IPv4, needed for some providers | |
|
||||||
| `INFLUXDB_ENABLED` | Enable the connection with InfluxDB | `false` |
|
| `INFLUXDB_ENABLED` | Enable the connection with InfluxDB | `false` |
|
||||||
| `INFLUXDB_URL` | InfluxDB URL | `http://localhost:8086` |
|
| `INFLUXDB_URL` | InfluxDB URL | `http://localhost:8086` |
|
||||||
| `INFLUXDB_TOKEN` | InfluxDB token | |
|
| `INFLUXDB_TOKEN` | InfluxDB token | |
|
||||||
| `INFLUXDB_BUCKET` | InfluxDB bucket name | `domainwatchdog` |
|
| `INFLUXDB_BUCKET` | InfluxDB bucket name | `domainwatchdog` |
|
||||||
| `INFLUXDB_ORG` | InfluxDB organization | `domainwatchdog` |
|
| `INFLUXDB_ORG` | InfluxDB organization | `domainwatchdog` |
|
||||||
| `LIMITED_FEATURES` | Limit certain features for users | `false` |
|
| `LIMITED_FEATURES` | Limit certain features for users | `false` |
|
||||||
| `LIMIT_MAX_WATCHLIST` | Maximum number of Watchlists per user | `0` |
|
| `LIMIT_MAX_WATCHLIST` | Maximum number of Watchlists per user | `0` |
|
||||||
| `LIMIT_MAX_WATCHLIST_DOMAINS` | Maximum number of domains per Watchlist | `0` |
|
| `LIMIT_MAX_WATCHLIST_DOMAINS` | Maximum number of domains per Watchlist | `0` |
|
||||||
| `LIMIT_MAX_WATCHLIST_WEBHOOKS` | Maximum number of webhooks per Watchlist | `0` |
|
| `LIMIT_MAX_WATCHLIST_WEBHOOKS` | Maximum number of webhooks per Watchlist | `0` |
|
||||||
| `MAILER_SENDER_NAME` | Name of the sender of emails | `Domain Watchdog` |
|
| `MAILER_SENDER_NAME` | Name of the sender of emails | `Domain Watchdog` |
|
||||||
| `MAILER_SENDER_EMAIL` | Sender's email address | `notifications@example.com` |
|
| `MAILER_SENDER_EMAIL` | Sender's email address | `notifications@example.com` |
|
||||||
| `REGISTRATION_ENABLED` | Enable user registration | `true` |
|
| `REGISTRATION_ENABLED` | Enable user registration | `true` |
|
||||||
| `REGISTRATION_VERIFY_EMAIL` | Verify email addresses during registration | `false` |
|
| `REGISTRATION_VERIFY_EMAIL` | Verify email addresses during registration | `false` |
|
||||||
| `MAILER_DSN` | Please check Symfony config | `null://null` |
|
| `MAILER_DSN` | Please check Symfony config | `null://null` |
|
||||||
| `OAUTH_CLIENT_ID` | Client ID (OAuth 2.0) for using external SSO | |
|
| `OAUTH_CLIENT_ID` | Client ID (OAuth 2.0) for using external SSO | |
|
||||||
| `OAUTH_CLIENT_SECRET` | Client secret (OAuth 2.0) | |
|
| `OAUTH_CLIENT_SECRET` | Client secret (OAuth 2.0) | |
|
||||||
| `OAUTH_AUTHORIZATION_URL` | Authorization URL (OAuth 2.0) | |
|
| `OAUTH_AUTHORIZATION_URL` | Authorization URL (OAuth 2.0) | |
|
||||||
| `OAUTH_TOKEN_URL` | Token URL (OAuth 2.0) | |
|
| `OAUTH_TOKEN_URL` | Token URL (OAuth 2.0) | |
|
||||||
| `OAUTH_USERINFO_URL` | User Info URL (OAuth 2.0) | |
|
| `OAUTH_USERINFO_URL` | User Info URL (OAuth 2.0) | |
|
||||||
| `OAUTH_SCOPE` | Scope (OAuth 2.0) | |
|
| `OAUTH_SCOPE` | Scope (OAuth 2.0) | |
|
||||||
| `SSO_AUTO_REDIRECT` | Redirection to the SSO auth URL | `false` |
|
| `SSO_AUTO_REDIRECT` | Redirect to the SSO auth URL | `false` |
|
||||||
|
| `PUBLIC_RDAP_LOOKUP_ENABLE` | Allow unauthenticated domain name name lookups | `false` |
|
||||||
|
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import {LinkCard} from '@astrojs/starlight/components';
|
|||||||
| `OAUTH_USERINFO_URL` | URL des informations utilisateur (OAuth 2.0) | |
|
| `OAUTH_USERINFO_URL` | URL des informations utilisateur (OAuth 2.0) | |
|
||||||
| `OAUTH_SCOPE` | Scope (OAuth 2.0) | |
|
| `OAUTH_SCOPE` | Scope (OAuth 2.0) | |
|
||||||
| `SSO_AUTO_REDIRECT` | Redirection vers l'URL d'authentification du SSO | `false` |
|
| `SSO_AUTO_REDIRECT` | Redirection vers l'URL d'authentification du SSO | `false` |
|
||||||
|
| `PUBLIC_RDAP_LOOKUP_ENABLE` | Autoriser les recherches anonymes de domaines | `false` |
|
||||||
|
|
||||||
|
|
||||||
## Authentification
|
## Authentification
|
||||||
|
|||||||
@ -15,7 +15,8 @@ class InstanceController extends AbstractController
|
|||||||
->setLimitedFeatures($this->getParameter('limited_features') ?? false)
|
->setLimitedFeatures($this->getParameter('limited_features') ?? false)
|
||||||
->setOauthEnabled($this->getParameter('oauth_enabled') ?? false)
|
->setOauthEnabled($this->getParameter('oauth_enabled') ?? false)
|
||||||
->setRegisterEnabled($this->getParameter('registration_enabled') ?? false)
|
->setRegisterEnabled($this->getParameter('registration_enabled') ?? false)
|
||||||
->setSsoAutoRedirect($this->getParameter('sso_auto_redirect') ?? false);
|
->setSsoAutoRedirect($this->getParameter('sso_auto_redirect') ?? false)
|
||||||
|
->setPublicRdapLookupEnabled($this->getParameter('public_rdap_lookup_enabled') ?? false);
|
||||||
|
|
||||||
return $instance;
|
return $instance;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,7 +71,7 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
provider: AutoRegisterDomainProvider::class
|
provider: AutoRegisterDomainProvider::class,
|
||||||
)]
|
)]
|
||||||
class Domain
|
class Domain
|
||||||
{
|
{
|
||||||
|
|||||||
@ -30,6 +30,8 @@ class Instance
|
|||||||
|
|
||||||
private ?bool $ssoAutoRedirect = null;
|
private ?bool $ssoAutoRedirect = null;
|
||||||
|
|
||||||
|
private ?bool $publicRdapLookupEnabled = null;
|
||||||
|
|
||||||
public function isSsoLogin(): ?bool
|
public function isSsoLogin(): ?bool
|
||||||
{
|
{
|
||||||
return $this->oauthEnabled;
|
return $this->oauthEnabled;
|
||||||
@ -77,4 +79,16 @@ class Instance
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getPublicRdapLookupEnabled(): ?bool
|
||||||
|
{
|
||||||
|
return $this->publicRdapLookupEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPublicRdapLookupEnabled(?bool $publicRdapLookupEnabled): static
|
||||||
|
{
|
||||||
|
$this->publicRdapLookupEnabled = $publicRdapLookupEnabled;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/Security/Voter/RdapLookupVoter.php
Normal file
33
src/Security/Voter/RdapLookupVoter.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Security\Voter;
|
||||||
|
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
|
class RdapLookupVoter extends Voter
|
||||||
|
{
|
||||||
|
public const string ATTRIBUTE = 'CAN_RDAP_LOOKUP';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly ParameterBagInterface $parameterBag,
|
||||||
|
private readonly Security $security,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function supports(string $attribute, mixed $subject): bool
|
||||||
|
{
|
||||||
|
return self::ATTRIBUTE === $attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||||
|
{
|
||||||
|
if ($this->security->isGranted('IS_AUTHENTICATED_FULLY')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->parameterBag->get('public_rdap_lookup_enabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -41,6 +41,7 @@ readonly class AutoRegisterDomainProvider implements ProviderInterface
|
|||||||
private KernelInterface $kernel,
|
private KernelInterface $kernel,
|
||||||
private ParameterBagInterface $parameterBag,
|
private ParameterBagInterface $parameterBag,
|
||||||
private RateLimiterFactory $userRdapRequestsLimiter,
|
private RateLimiterFactory $userRdapRequestsLimiter,
|
||||||
|
private RateLimiterFactory $publicRdapRequestsLimiter,
|
||||||
private Security $security,
|
private Security $security,
|
||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
private DomainRepository $domainRepository,
|
private DomainRepository $domainRepository,
|
||||||
@ -68,14 +69,20 @@ readonly class AutoRegisterDomainProvider implements ProviderInterface
|
|||||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
|
||||||
{
|
{
|
||||||
$fromWatchlist = array_key_exists('root_operation', $context) && Watchlist::class === $context['root_operation']?->getClass();
|
$fromWatchlist = array_key_exists('root_operation', $context) && Watchlist::class === $context['root_operation']?->getClass();
|
||||||
|
|
||||||
$userId = $this->security->getUser()->getUserIdentifier();
|
|
||||||
$idnDomain = RDAPService::convertToIdn($uriVariables['ldhName']);
|
$idnDomain = RDAPService::convertToIdn($uriVariables['ldhName']);
|
||||||
|
|
||||||
$this->logger->info('User wants to update a domain name', [
|
$user = $this->security->getUser();
|
||||||
'username' => $userId,
|
|
||||||
'ldhName' => $idnDomain,
|
if (null !== $user) {
|
||||||
]);
|
$this->logger->info('User wants to update a domain name', [
|
||||||
|
'username' => $user->getUserIdentifier(),
|
||||||
|
'ldhName' => $idnDomain,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$this->logger->info('Anonymous wants to update a domain name', [
|
||||||
|
'ldhName' => $idnDomain,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$request = $this->requestStack->getCurrentRequest();
|
$request = $this->requestStack->getCurrentRequest();
|
||||||
|
|
||||||
@ -96,7 +103,7 @@ readonly class AutoRegisterDomainProvider implements ProviderInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (false === $this->kernel->isDebug() && true === $this->parameterBag->get('limited_features')) {
|
if (false === $this->kernel->isDebug() && true === $this->parameterBag->get('limited_features')) {
|
||||||
$limiter = $this->userRdapRequestsLimiter->create($userId);
|
$limiter = $user ? $this->userRdapRequestsLimiter->create($user->getUserIdentifier()) : $this->publicRdapRequestsLimiter->create($request->getClientIp());
|
||||||
$limit = $limiter->consume();
|
$limit = $limiter->consume();
|
||||||
|
|
||||||
if (!$limit->isAccepted()) {
|
if (!$limit->isAccepted()) {
|
||||||
|
|||||||
@ -27,6 +27,15 @@ final class AutoRegisterDomainProviderTest extends ApiTestCase
|
|||||||
$this->assertMatchesResourceItemJsonSchema(Domain::class);
|
$this->assertMatchesResourceItemJsonSchema(Domain::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[DependsExternal(RDAPServiceTest::class, 'testUpdateRdapServers')]
|
||||||
|
public function testRegisterDomainAnonymousUnauthorized(): void
|
||||||
|
{
|
||||||
|
$client = $this->createClient();
|
||||||
|
$client->request('GET', '/api/domains/example.com');
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(401);
|
||||||
|
}
|
||||||
|
|
||||||
#[DependsExternal(RDAPServiceTest::class, 'testUpdateRdapServers')]
|
#[DependsExternal(RDAPServiceTest::class, 'testUpdateRdapServers')]
|
||||||
public function testRegisterDomainAlreadyUpdated(): void
|
public function testRegisterDomainAlreadyUpdated(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@ -3,31 +3,35 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n!=1);\n"
|
"Plural-Forms: nplurals=2; plural=(n!=1);\n"
|
||||||
|
|
||||||
#: assets/App.tsx:161
|
#: assets/App.tsx:114
|
||||||
|
msgid "Unable to contact the server, please reload the page."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/App.tsx:174
|
||||||
msgid "TOS"
|
msgid "TOS"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/App.tsx:162
|
#: assets/App.tsx:175
|
||||||
msgid "Privacy Policy"
|
msgid "Privacy Policy"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/App.tsx:163
|
#: assets/App.tsx:176
|
||||||
msgid "FAQ"
|
msgid "FAQ"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/App.tsx:169
|
#: assets/App.tsx:182
|
||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/App.tsx:177
|
#: assets/App.tsx:190
|
||||||
msgid "Source code"
|
msgid "Source code"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/App.tsx:185
|
#: assets/App.tsx:198
|
||||||
msgid "Submit an issue"
|
msgid "Submit an issue"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/App.tsx:190
|
#: assets/App.tsx:203
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"${ ProjectLink } is an open source project distributed under the ${ "
|
"${ ProjectLink } is an open source project distributed under the ${ "
|
||||||
@ -87,7 +91,7 @@ msgid "Log in with SSO"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/RegisterForm.tsx:54
|
#: assets/components/RegisterForm.tsx:54
|
||||||
#: assets/pages/LoginPage.tsx:60
|
#: assets/pages/LoginPage.tsx:50
|
||||||
msgid "Register"
|
msgid "Register"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -168,60 +172,60 @@ msgstr ""
|
|||||||
msgid "This domain name does not appear to be valid"
|
msgid "This domain name does not appear to be valid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:31
|
#: assets/components/Sider.tsx:35
|
||||||
msgid "Home"
|
msgid "Home"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:37
|
#: assets/components/Sider.tsx:41
|
||||||
msgid "Search"
|
msgid "Search"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:43
|
#: assets/components/Sider.tsx:47
|
||||||
#: assets/components/tracking/watchlist/TrackedDomainTable.tsx:179
|
#: assets/components/tracking/watchlist/TrackedDomainTable.tsx:179
|
||||||
msgid "Domain"
|
msgid "Domain"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:44
|
#: assets/components/Sider.tsx:48
|
||||||
msgid "Domain Finder"
|
msgid "Domain Finder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:63
|
#: assets/components/Sider.tsx:67
|
||||||
#: assets/components/Sider.tsx:64
|
#: assets/components/Sider.tsx:68
|
||||||
msgid "Infrastructure"
|
msgid "Infrastructure"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:70
|
#: assets/components/Sider.tsx:74
|
||||||
#: assets/pages/StatisticsPage.tsx:112
|
#: assets/pages/StatisticsPage.tsx:112
|
||||||
#: assets/pages/infrastructure/TldPage.tsx:81
|
#: assets/pages/infrastructure/TldPage.tsx:81
|
||||||
msgid "TLD"
|
msgid "TLD"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:71
|
#: assets/components/Sider.tsx:75
|
||||||
msgid "TLD list"
|
msgid "TLD list"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:78
|
#: assets/components/Sider.tsx:82
|
||||||
#: assets/components/Sider.tsx:79
|
#: assets/components/Sider.tsx:83
|
||||||
msgid "ICANN list"
|
msgid "ICANN list"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:87
|
#: assets/components/Sider.tsx:91
|
||||||
msgid "Tracking"
|
msgid "Tracking"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:93
|
#: assets/components/Sider.tsx:97
|
||||||
msgid "My Watchlists"
|
msgid "My Watchlists"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:100
|
#: assets/components/Sider.tsx:104
|
||||||
msgid "Tracking table"
|
msgid "Tracking table"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:107
|
#: assets/components/Sider.tsx:111
|
||||||
msgid "My Connectors"
|
msgid "My Connectors"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:131
|
#: assets/components/Sider.tsx:135
|
||||||
#.
|
#.
|
||||||
#. {
|
#. {
|
||||||
#. key: 'tools',
|
#. key: 'tools',
|
||||||
@ -240,18 +244,18 @@ msgstr ""
|
|||||||
msgid "Statistics"
|
msgid "Statistics"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:141
|
#: assets/components/Sider.tsx:145
|
||||||
#: assets/pages/UserPage.tsx:17
|
#: assets/pages/UserPage.tsx:17
|
||||||
msgid "My Account"
|
msgid "My Account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:146
|
#: assets/components/Sider.tsx:150
|
||||||
msgid "Log out"
|
msgid "Log out"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/components/Sider.tsx:154
|
#: assets/components/Sider.tsx:158
|
||||||
#: assets/pages/LoginPage.tsx:46
|
#: assets/pages/LoginPage.tsx:36
|
||||||
#: assets/pages/LoginPage.tsx:60
|
#: assets/pages/LoginPage.tsx:50
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -690,7 +694,7 @@ msgstr ""
|
|||||||
msgid "Sponsored Top-Level-Domains"
|
msgid "Sponsored Top-Level-Domains"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/pages/LoginPage.tsx:46
|
#: assets/pages/LoginPage.tsx:36
|
||||||
msgid "Create an account"
|
msgid "Create an account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -698,35 +702,35 @@ msgstr ""
|
|||||||
msgid "Sorry, the page you visited does not exist."
|
msgid "Sorry, the page you visited does not exist."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/pages/search/DomainSearchPage.tsx:36
|
#: assets/pages/search/DomainSearchPage.tsx:37
|
||||||
msgid "Found !"
|
msgid "Found !"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/pages/search/DomainSearchPage.tsx:53
|
#: assets/pages/search/DomainSearchPage.tsx:54
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "${ ldhName } added to ${ watchlist.name }"
|
msgid "${ ldhName } added to ${ watchlist.name }"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/pages/search/DomainSearchPage.tsx:70
|
#: assets/pages/search/DomainSearchPage.tsx:71
|
||||||
msgid ""
|
msgid ""
|
||||||
"Although the domain exists in my database, it has been deleted from the "
|
"Although the domain exists in my database, it has been deleted from the "
|
||||||
"WHOIS by its registrar."
|
"WHOIS by its registrar."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/pages/search/DomainSearchPage.tsx:82
|
#: assets/pages/search/DomainSearchPage.tsx:83
|
||||||
msgid "Add to Watchlist"
|
msgid "Add to Watchlist"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/pages/search/DomainSearchPage.tsx:92
|
#: assets/pages/search/DomainSearchPage.tsx:93
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Add ${ domainLdhName } to a Watchlist"
|
msgid "Add ${ domainLdhName } to a Watchlist"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/pages/search/DomainSearchPage.tsx:95
|
#: assets/pages/search/DomainSearchPage.tsx:96
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: assets/pages/search/DomainSearchPage.tsx:96
|
#: assets/pages/search/DomainSearchPage.tsx:97
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user