Skip to content

Commit

Permalink
feat: add polar provider
Browse files Browse the repository at this point in the history
* feat: add polar provider

* up

---------

Co-authored-by: Sébastien Chopin <[email protected]>
  • Loading branch information
ahmedrangel and atinux authored Oct 1, 2024
1 parent 3bd76b0 commit 2682bcb
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ It can also be set using environment variables:
- LinkedIn
- Microsoft
- PayPal
- Polar
- Spotify
- Steam
- TikTok
Expand Down
3 changes: 3 additions & 0 deletions playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,6 @@ NUXT_OAUTH_TIKTOK_CLIENT_SECRET=
# Dropbox
NUXT_OAUTH_DROPBOX_CLIENT_ID=
NUXT_OAUTH_DROPBOX_CLIENT_SECRET=
# Polar
NUXT_OAUTH_POLAR_CLIENT_ID=
NUXT_OAUTH_POLAR_CLIENT_SECRET=
6 changes: 6 additions & 0 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ const providers = computed(() =>
disabled: Boolean(user.value?.dropbox),
icon: 'i-simple-icons-dropbox',
},
{
label: user.value?.polar || 'Polar',
to: '/auth/polar',
disabled: Boolean(user.value?.polar),
icon: 'i-iconoir-polar-sh',
},
].map(p => ({
...p,
prefetch: false,
Expand Down
1 change: 1 addition & 0 deletions playground/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ declare module '#auth-utils' {
yandex?: string
tiktok?: string
dropbox?: string
polar?: string
}

interface UserSession {
Expand Down
3 changes: 2 additions & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
"generate": "nuxi generate"
},
"dependencies": {
"@iconify-json/gravity-ui": "^1.2.1",
"@iconify-json/iconoir": "^1.2.1",
"nuxt": "^3.13.2",
"nuxt-auth-utils": "latest",
"zod": "^3.23.8"
},
"devDependencies": {
"@iconify-json/gravity-ui": "^1.2.1",
"better-sqlite3": "^11.2.1"
}
}
15 changes: 15 additions & 0 deletions playground/server/routes/auth/polar.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default defineOAuthPolarEventHandler({
config: {
emailRequired: true,
},
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
polar: user.email,
},
loggedInAt: Date.now(),
})

return sendRedirect(event, '/')
},
})
16 changes: 13 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,5 +288,11 @@ export default defineNuxtModule<ModuleOptions>({
clientSecret: '',
redirectURL: '',
})
// Polar OAuth
runtimeConfig.oauth.polar = defu(runtimeConfig.oauth.polar, {
clientId: '',
clientSecret: '',
redirectURL: '',
})
},
})
136 changes: 136 additions & 0 deletions src/runtime/server/lib/oauth/polar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import type { H3Event } from 'h3'
import { eventHandler, getQuery, sendRedirect } from 'h3'
import { withQuery } from 'ufo'
import { defu } from 'defu'
import { handleAccessTokenErrorResponse, handleMissingConfiguration, getOAuthRedirectURL, requestAccessToken } from '../utils'
import { useRuntimeConfig, createError } from '#imports'
import type { OAuthConfig } from '#auth-utils'

export interface OAuthPolarConfig {
/**
* Polar Client ID
* @default process.env.NUXT_OAUTH_POLAR_CLIENT_ID
*/
clientId?: string

/**
* Polar OAuth Client Secret
* @default process.env.NUXT_OAUTH_POLAR_CLIENT_SECRET
*/
clientSecret?: string

/**
* Polar OAuth Scope
* @default []
* @see https://api.polar.sh/.well-known/openid-configuration
* @example ['openid']
*/
scope?: string[]

/**
* Require email from user, adds the ['email'] scope if not present
* @default false
*/
emailRequired?: boolean

/**
* Polar OAuth Authorization URL
* @see https://docs.polar.sh/api/authentication#start-the-authorization-flow
* @default 'https://polar.sh/oauth2/authorize'
*/
authorizationURL?: string

/**
* Polar OAuth Token URL
* @see https://docs.polar.sh/api/authentication#exchange-the-authorization-code
* @default 'https://api.polar.sh/v1/oauth2/token'
*/
tokenURL?: string

/**
* Extra authorization parameters to provide to the authorization URL
* @see https://docs.polar.sh/api/authentication#user-vs-organization-access-tokens
* @example { sub_type: 'organization' }
*/
authorizationParams?: Record<string, string>

/**
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
* @default process.env.NUXT_OAUTH_POLAR_REDIRECT_URL or current URL
*/
redirectURL?: string
}

export function defineOAuthPolarEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthPolarConfig>) {
return eventHandler(async (event: H3Event) => {
config = defu(config, useRuntimeConfig(event).oauth?.polar, {
authorizationURL: 'https://polar.sh/oauth2/authorize',
tokenURL: 'https://api.polar.sh/v1/oauth2/token',
}) as OAuthPolarConfig
const query = getQuery<{ code?: string }>(event)
if (!config.clientId || !config.clientSecret) {
return handleMissingConfiguration(event, 'polar', ['clientId', 'clientSecret'], onError)
}

const redirectURL = config.redirectURL || getOAuthRedirectURL(event)

if (!query.code) {
config.scope = config.scope || []
if (!config.scope.includes('openid'))
config.scope.push('openid')
if (config.emailRequired && !config.scope.includes('email'))
config.scope.push('email')

// Redirect to Polar Oauth page
return sendRedirect(
event,
withQuery(config.authorizationURL as string, {
response_type: 'code',
client_id: config.clientId,
redirect_uri: redirectURL,
scope: config.scope.join(' '),
...config.authorizationParams,
}),
)
}

const tokens = await requestAccessToken(config.tokenURL as string, {
body: {
grant_type: 'authorization_code',
redirect_uri: redirectURL,
client_id: config.clientId,
client_secret: config.clientSecret,
code: query.code,
},
})

if (tokens.error) {
return handleAccessTokenErrorResponse(event, 'polar', tokens, onError)
}
const accessToken = tokens.access_token

// TODO: improve typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const user: any = await $fetch('https://api.polar.sh/v1/oauth2/userinfo', {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})

if (!user) {
const error = createError({
statusCode: 500,
message: 'Could not get Polar user',
data: tokens,
})
if (!onError) throw error
return onError(event, error)
}

return onSuccess(event, {
tokens,
user,
})
})
}
2 changes: 1 addition & 1 deletion src/runtime/types/oauth-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { H3Event, H3Error } from 'h3'

export type OAuthProvider = 'auth0' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'github' | 'gitlab' | 'google' | 'instagram' | 'keycloak' | 'linkedin' | 'microsoft' | 'paypal' | 'spotify' | 'steam' | 'tiktok' | 'twitch' | 'vk' | 'x' | 'xsuaa' | 'yandex' | (string & {})
export type OAuthProvider = 'auth0' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'github' | 'gitlab' | 'google' | 'instagram' | 'keycloak' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'steam' | 'tiktok' | 'twitch' | 'vk' | 'x' | 'xsuaa' | 'yandex' | (string & {})

export type OnError = (event: H3Event, error: H3Error) => Promise<void> | void

Expand Down

0 comments on commit 2682bcb

Please sign in to comment.