@rsweeten/dropbox-sync
Version:
Reusable Dropbox synchronization module with framework adapters
149 lines (128 loc) • 5.12 kB
text/typescript
import { Dropbox } from 'dropbox'
import type { AuthMethods, DropboxCredentials, TokenResponse } from './types'
// Define missing types for Dropbox SDK
interface DropboxAuthExtended {
getAuthenticationUrl(
redirectUri: string,
state: string,
authType?: string,
tokenAccessType?: string,
scope?: string | string[],
includeGrantedScopes?: string,
usePKCE?: boolean
): string
getAccessTokenFromCode(
redirectUri: string,
code: string
): Promise<{
result: {
access_token: string
refresh_token?: string
expires_in?: number
}
}>
}
export function createAuthMethods(
getClient: () => Dropbox,
credentials: DropboxCredentials,
setAccessToken: (token: string) => void
): AuthMethods {
return {
/**
* Generate an authentication URL for Dropbox OAuth flow
*/
async getAuthUrl(redirectUri: string, state?: string): Promise<string> {
const dropbox: any = getClient()
const randomState =
state || Math.random().toString(36).substring(2, 15)
// Cast to extended auth type to access missing methods
const auth = dropbox.auth as unknown as DropboxAuthExtended
// Generate the authentication URL
const authUrl = auth.getAuthenticationUrl(
redirectUri,
randomState,
'code', // Use authorization code flow
'offline', // Request a refresh token for long-lived access
undefined, // No scope specified, request full access
undefined, // No include_granted_scopes
true // Force reapproval to ensure we get fresh tokens
)
return authUrl
},
/**
* Exchange authorization code for access token
*/
async exchangeCodeForToken(
code: string,
redirectUri: string
): Promise<TokenResponse> {
const dropbox: any = getClient()
// Cast to extended auth type to access missing methods
const auth = dropbox.auth as unknown as DropboxAuthExtended
// Exchange the code for an access token
const response = await auth.getAccessTokenFromCode(
redirectUri,
code
)
const tokenResponse = {
accessToken: response.result.access_token,
refreshToken: response.result.refresh_token,
expiresAt: response.result.expires_in
? Date.now() + response.result.expires_in * 1000
: undefined,
}
// Update the client with the new token
setAccessToken(tokenResponse.accessToken)
return tokenResponse
},
/**
* Refresh an expired access token using the refresh token
*/
async refreshAccessToken(): Promise<TokenResponse> {
if (
!credentials.refreshToken ||
!credentials.clientId ||
!credentials.clientSecret
) {
throw new Error(
'Refresh token, client ID, and client secret are required to refresh access token'
)
}
// Dropbox API endpoint for token refresh
const tokenUrl = 'https://api.dropboxapi.com/oauth2/token'
// Prepare the form data for the token request
const formData = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: credentials.refreshToken,
client_id: credentials.clientId,
client_secret: credentials.clientSecret,
})
// Make the request to refresh the token
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData.toString(),
})
if (!response.ok) {
await response.text() // Consume the response body
throw new Error(
`Failed to refresh token: ${response.status} ${response.statusText}`
)
}
// Parse the response
const data = await response.json()
const tokenResponse = {
accessToken: data.access_token,
refreshToken: data.refresh_token || credentials.refreshToken,
expiresAt: data.expires_in
? Date.now() + data.expires_in * 1000
: undefined,
}
// Update the client with the new token
setAccessToken(tokenResponse.accessToken)
return tokenResponse
},
}
}