feat: allow unauthenticated users to perform domain name lookups

This commit is contained in:
Maël Gangloff
2025-12-08 18:18:33 +01:00
parent eddb267275
commit 5476ee7acc
16 changed files with 214 additions and 110 deletions

View File

@@ -9,8 +9,8 @@ import WatchlistPage from './pages/tracking/WatchlistPage'
import UserPage from './pages/UserPage'
import type {PropsWithChildren} from 'react'
import React, { useCallback, useEffect, useMemo, useState} from 'react'
import {getUser} from './utils/api'
import LoginPage, {AuthenticatedContext} from './pages/LoginPage'
import {getConfiguration, getUser, type InstanceConfig} from './utils/api'
import LoginPage from './pages/LoginPage'
import ConnectorPage from './pages/tracking/ConnectorPage'
import NotFoundPage from './pages/NotFoundPage'
import useBreakpoint from './hooks/useBreakpoint'
@@ -19,12 +19,14 @@ import {jt, t} from 'ttag'
import {MenuOutlined} from '@ant-design/icons'
import TrackedDomainPage from './pages/tracking/TrackedDomainPage'
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 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 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 {
const {sidebarCollapsed, setSidebarCollapsed, children} = props
@@ -68,18 +70,21 @@ export default function App(): React.ReactElement {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
const [isAuthenticated, setIsAuthenticated] = useState(false)
const authenticated = useCallback((authenticated: boolean) => {
setIsAuthenticated(authenticated)
}, [])
const contextValue = useMemo(() => ({
authenticated,
setIsAuthenticated
}), [authenticated, setIsAuthenticated])
const [configuration, setConfiguration] = useState<InstanceConfig | undefined>(undefined)
const [darkMode, setDarkMode] = useState(false)
const windowQuery = window.matchMedia('(prefers-color-scheme:dark)')
const authContextValue: AuthContextType = useMemo(() => ({
isAuthenticated,
setIsAuthenticated
}), [isAuthenticated])
const configContextValue = useMemo(() => ({
configuration,
}), [configuration])
const darkModeChange = useCallback((event: MediaQueryListEvent) => {
setDarkMode(event.matches)
}, [])
@@ -93,13 +98,18 @@ export default function App(): React.ReactElement {
useEffect(() => {
setDarkMode(windowQuery.matches)
getUser().then(() => {
setIsAuthenticated(true)
if (location.pathname === '/login') navigate('/home')
}).catch(() => {
setIsAuthenticated(false)
const pathname = location.pathname
if (!['/login', '/tos', '/faq', '/privacy'].includes(pathname)) navigate('/home')
getConfiguration().then(configuration => {
setConfiguration(configuration)
getUser().then(() => {
setIsAuthenticated(true)
if (location.pathname === '/login') 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')
})
})
}, [])
@@ -109,10 +119,11 @@ export default function App(): React.ReactElement {
algorithm: darkMode ? theme.darkAlgorithm : undefined
}}
>
<AuthenticatedContext.Provider value={contextValue}>
<ConfigurationContext.Provider value={configContextValue}>
<AuthenticatedContext.Provider value={authContextValue}>
<Layout hasSider style={{minHeight: '100vh'}}>
<SiderWrapper sidebarCollapsed={sidebarCollapsed} setSidebarCollapsed={setSidebarCollapsed}>
<Sider isAuthenticated={isAuthenticated}/>
<Sider />
</SiderWrapper>
<Layout>
<Layout.Header style={{padding: 0}}>
@@ -129,7 +140,7 @@ export default function App(): React.ReactElement {
}}
>
<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='/search/domain' element={<DomainSearchPage/>}/>
@@ -158,8 +169,8 @@ export default function App(): React.ReactElement {
</Layout.Content>
<Layout.Footer style={{textAlign: 'center'}}>
<Flex gap='middle' wrap justify='center'>
<Link to='/tos'><Button type='text'>{t`TOS`}</Button></Link>
<Link to='/privacy'><Button type='text'>{t`Privacy Policy`}</Button></Link>
<Link to='/tos' rel='terms-of-service'><Button type='text'>{t`TOS`}</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>
<Typography.Link
target='_blank'
@@ -193,6 +204,7 @@ export default function App(): React.ReactElement {
</Layout>
</Layout>
</AuthenticatedContext.Provider>
</ConfigurationContext.Provider>
</ConfigProvider>
)
}

View File

@@ -2,10 +2,10 @@ import {Button, Flex, Form, Input, message} from 'antd'
import {t} from 'ttag'
import React, {useContext, useEffect, useState} from 'react'
import {getUser, login} from '../utils/api'
import {AuthenticatedContext} from '../pages/LoginPage'
import {useNavigate} from 'react-router-dom'
import {showErrorAPI} from '../utils/functions/showErrorAPI'
import {AuthenticatedContext} from "../contexts"
interface FieldType {
username: string
@@ -66,9 +66,9 @@ export function LoginForm({ssoLogin}: { ssoLogin?: boolean }) {
</Form.Item>
<Flex wrap justify="center" gap="middle">
<Button type='primary' htmlType='submit'>
{t`Submit`}
</Button>
<Button type='primary' htmlType='submit'>
{t`Submit`}
</Button>
{ssoLogin &&
<Button type='dashed' htmlType='button' href='/login/oauth'>
{t`Log in with SSO`}

View File

@@ -17,10 +17,14 @@ import {
UserOutlined
} from '@ant-design/icons'
import {Menu} from 'antd'
import React from 'react'
import React, {useContext} from 'react'
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 location = useLocation()
@@ -42,7 +46,7 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
icon: <CompassOutlined/>,
label: t`Domain`,
title: t`Domain Finder`,
disabled: !isAuthenticated,
disabled: !configuration?.publicRdapLookupEnabled && !isAuthenticated,
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 {t} from 'ttag'
import TextPage from './TextPage'
import {LoginForm} from '../components/LoginForm'
import type { InstanceConfig} from '../utils/api'
import {getConfiguration} from '../utils/api'
import {RegisterForm} from '../components/RegisterForm'
import useBreakpoint from "../hooks/useBreakpoint"
export const AuthenticatedContext = createContext<
{
authenticated: (authenticated: boolean) => void
setIsAuthenticated: React.Dispatch<React.SetStateAction<boolean>>
}
>({
authenticated: () => {
},
setIsAuthenticated: () => {
}
})
import {ConfigurationContext} from "../contexts"
export default function LoginPage() {
const [wantRegister, setWantRegister] = useState<boolean>(false)
const [configuration, setConfiguration] = useState<InstanceConfig>()
const { configuration } = useContext(ConfigurationContext)
const md = useBreakpoint('md')
const toggleWantRegister = () => {
@@ -30,14 +18,11 @@ export default function LoginPage() {
}
useEffect(() => {
getConfiguration().then((configuration) => {
if(!configuration.registerEnabled && configuration.ssoLogin && configuration.ssoAutoRedirect) {
window.location.href = '/login/oauth'
return
}
setConfiguration(configuration)
})
}, [])
if(!configuration?.registerEnabled && configuration?.ssoLogin && configuration?.ssoAutoRedirect) {
window.location.href = '/login/oauth'
return
}
}, [configuration])
const grid = [
<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 type { FormProps} from 'antd'
import {FloatButton} from 'antd'
import {Empty, Flex, message, Skeleton} from 'antd'
import React, {useContext, useEffect, useState} from 'react'
import type {FormProps} from 'antd'
import {Empty, Flex, FloatButton, message, Skeleton} from 'antd'
import type {Domain, Watchlist} from '../../utils/api'
import {addDomainToWatchlist} from '../../utils/api'
import {getDomain} from '../../utils/api'
import {addDomainToWatchlist, getDomain} from '../../utils/api'
import type {AxiosError} from 'axios'
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 {DomainResult} from '../../components/search/DomainResult'
import {showErrorAPI} from '../../utils/functions/showErrorAPI'
import {useNavigate, useParams} from 'react-router-dom'
import {PlusOutlined} from '@ant-design/icons'
import WatchlistSelectionModal from '../../components/tracking/watchlist/WatchlistSelectionModal'
import {AuthenticatedContext} from "../../contexts"
export default function DomainSearchPage() {
const {query} = useParams()
@@ -21,6 +20,8 @@ export default function DomainSearchPage() {
const domainLdhName = domain?.ldhName
const [loading, setLoading] = useState(false)
const [addToWatchlistModal, setAddToWatchlistModal] = useState(false)
const {isAuthenticated} = useContext(AuthenticatedContext)
const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate()
@@ -72,7 +73,7 @@ export default function DomainSearchPage() {
}
</Skeleton>
</Flex>
{domain
{domain && isAuthenticated
&& <FloatButton
style={{
position: 'fixed',
@@ -94,7 +95,7 @@ export default function DomainSearchPage() {
onClose: () => setAddToWatchlistModal(false),
cancelText: t`Cancel`,
okText: t`Add`
}}
}}
/>
</>
}

View File

@@ -109,6 +109,7 @@ export interface InstanceConfig {
ssoLogin: boolean
limtedFeatures: boolean
registerEnabled: boolean
publicRdapLookupEnabled: boolean
}
export interface Statistics {