-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Prevent use of another user's server-config (#241)
* Prevent use of another user's server-config * do not read from localstorage on server
- Loading branch information
1 parent
c419db8
commit 6e7de29
Showing
4 changed files
with
132 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 63 additions & 57 deletions
120
components/shell/ConfigurationContext/useServerConfigQuery.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,71 +1,77 @@ | ||
import { Agent } from '@atproto/api' | ||
import { ResponseType, XRPCError } from '@atproto/xrpc' | ||
import { useQuery } from '@tanstack/react-query' | ||
import { useEffect } from 'react' | ||
import { useLocalStorage } from 'react-use' | ||
|
||
import { parseServerConfig, ServerConfig } from '@/lib/server-config' | ||
import { parseServerConfig } from '@/lib/server-config' | ||
import { useStoredQuery } from '@/lib/useStoredQuery' | ||
|
||
export function useServerConfigQuery(agent: Agent) { | ||
const [cachedServerConfig, setCachedServerConfig] = | ||
useLocalStorage<ServerConfig>('labeler-server-config') | ||
|
||
const response = useQuery({ | ||
retry: (failureCount, error): boolean => { | ||
if (error instanceof XRPCError) { | ||
if (error.status === ResponseType.InternalServerError) { | ||
// The server is misconfigured | ||
return false | ||
} | ||
|
||
if ( | ||
error.status === ResponseType.InvalidRequest && | ||
error.message === 'could not resolve proxy did service url' | ||
) { | ||
// Labeler service not configured in the user's DID document (yet) | ||
return false | ||
} | ||
|
||
if (error.status === ResponseType.AuthRequired) { | ||
// User is logged in with a user that is not member of the labeler's | ||
// group. | ||
return false | ||
} | ||
} | ||
|
||
return failureCount < 3 | ||
}, | ||
retryDelay: (attempt, error) => { | ||
if ( | ||
error instanceof XRPCError && | ||
error.status === ResponseType.RateLimitExceeded && | ||
error.headers?.['ratelimit-remaining'] === '0' && | ||
error.headers?.['ratelimit-reset'] | ||
) { | ||
// ratelimit-limit: 3000 | ||
// ratelimit-policy: 3000;w=300 | ||
// ratelimit-remaining: 2977 | ||
// ratelimit-reset: 1724927309 | ||
|
||
const reset = Number(error.headers['ratelimit-reset']) * 1e3 | ||
return reset - Date.now() | ||
} | ||
|
||
// Exponential backoff with a maximum of 30 seconds | ||
return Math.min(1000 * 2 ** attempt, 30000) | ||
}, | ||
queryKey: ['server-config', agent.assertDid, agent.proxy], | ||
return useStoredQuery({ | ||
queryKey: ['server-config', agent.assertDid, agent.proxy ?? null], | ||
queryFn: async ({ signal }) => { | ||
const { data } = await agent.tools.ozone.server.getConfig({}, { signal }) | ||
return parseServerConfig(data) | ||
}, | ||
initialData: cachedServerConfig, | ||
retry, | ||
retryDelay, | ||
refetchOnWindowFocus: false, | ||
// Initialize with data from the legacy key (can be removed in the future) | ||
initialData: | ||
typeof window === 'undefined' | ||
? undefined | ||
: ((legacyKey: string) => { | ||
try { | ||
const data = localStorage.getItem(legacyKey) | ||
if (data) return JSON.parse(data) | ||
} catch { | ||
// Ignore | ||
} finally { | ||
localStorage.removeItem(legacyKey) | ||
} | ||
})('labeler-server-config'), | ||
}) | ||
} | ||
|
||
const retry = (failureCount: number, error: unknown): boolean => { | ||
if (error instanceof XRPCError) { | ||
if (error.status === ResponseType.InternalServerError) { | ||
// The server is misconfigured | ||
return false | ||
} | ||
|
||
if ( | ||
error.status === ResponseType.InvalidRequest && | ||
error.message === 'could not resolve proxy did service url' | ||
) { | ||
// Labeler service not configured in the user's DID document (yet) | ||
return false | ||
} | ||
|
||
if (error.status === ResponseType.AuthRequired) { | ||
// User is logged in with a user that is not member of the labeler's | ||
// group. | ||
return false | ||
} | ||
} | ||
|
||
return failureCount < 3 | ||
} | ||
|
||
const retryDelay = (attempt: number, error: unknown): number => { | ||
if ( | ||
error instanceof XRPCError && | ||
error.status === ResponseType.RateLimitExceeded && | ||
error.headers?.['ratelimit-remaining'] === '0' && | ||
error.headers?.['ratelimit-reset'] | ||
) { | ||
// ratelimit-limit: 3000 | ||
// ratelimit-policy: 3000;w=300 | ||
// ratelimit-remaining: 2977 | ||
// ratelimit-reset: 1724927309 | ||
|
||
useEffect(() => { | ||
if (response.data) setCachedServerConfig(response.data) | ||
}, [response.data, setCachedServerConfig]) | ||
const reset = Number(error.headers['ratelimit-reset']) * 1e3 | ||
return reset - Date.now() | ||
} | ||
|
||
return response | ||
// Exponential backoff with a maximum of 30 seconds | ||
return Math.min(1000 * 2 ** attempt, 30000) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { | ||
useQuery, | ||
UseQueryOptions, | ||
UseQueryResult, | ||
} from '@tanstack/react-query' | ||
import { useEffect } from 'react' | ||
import { useLocalStorage } from 'react-use' | ||
|
||
export function useStoredQuery< | ||
TData extends NonNullable<unknown> | null, | ||
TError, | ||
TQueryKey extends (string | number | boolean | null)[], | ||
>({ | ||
initialData, | ||
...options | ||
}: Omit< | ||
UseQueryOptions<TData, TError, TData, TQueryKey>, | ||
'queryKey' | 'initialData' | ||
> & { | ||
queryKey: TQueryKey | ||
initialData?: TData | ||
}): UseQueryResult<TData, TError> { | ||
const key = `storedQuery:${JSON.stringify(options.queryKey).slice(1, -1)}` | ||
|
||
const [storedData, setStoredData] = useLocalStorage<TData>(key, initialData) | ||
|
||
const response = useQuery({ ...options, initialData: storedData }) | ||
|
||
useEffect(() => { | ||
setStoredData(response.data) | ||
}, [response.data, setStoredData]) | ||
|
||
return response | ||
} |