Merge branch 'feat/anonym-domain-lookup' into develop

This commit is contained in:
Maël Gangloff 2025-12-09 13:43:01 +01:00
commit 43b0253eab
No known key found for this signature in database
GPG Key ID: 11FDC81C24A7F629
20 changed files with 258 additions and 137 deletions

1
.env
View File

@ -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

View File

@ -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>
) )
} }

View File

@ -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`}

View File

@ -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
View 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: () => {
},
})

View File

@ -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}>

View File

@ -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`
}} }}
/> />
</> </>
} }

View File

@ -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 {

View File

@ -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'

View File

@ -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 }

View File

@ -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)%'

View File

@ -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

View File

@ -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

View File

@ -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;
} }

View File

@ -71,7 +71,7 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
], ],
), ),
], ],
provider: AutoRegisterDomainProvider::class provider: AutoRegisterDomainProvider::class,
)] )]
class Domain class Domain
{ {

View File

@ -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;
}
} }

View 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');
}
}

View File

@ -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()) {

View File

@ -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
{ {

View File

@ -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 ""