Skip to content

Commit 2682bcb

Browse files
ahmedrangelatinux
andauthored
feat: add polar provider
* feat: add polar provider * up --------- Co-authored-by: Sébastien Chopin <[email protected]>
1 parent 3bd76b0 commit 2682bcb

File tree

10 files changed

+184
-5
lines changed

10 files changed

+184
-5
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ It can also be set using environment variables:
218218
- LinkedIn
219219
- Microsoft
220220
- PayPal
221+
- Polar
221222
- Spotify
222223
- Steam
223224
- TikTok

playground/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,6 @@ NUXT_OAUTH_TIKTOK_CLIENT_SECRET=
7171
# Dropbox
7272
NUXT_OAUTH_DROPBOX_CLIENT_ID=
7373
NUXT_OAUTH_DROPBOX_CLIENT_SECRET=
74+
# Polar
75+
NUXT_OAUTH_POLAR_CLIENT_ID=
76+
NUXT_OAUTH_POLAR_CLIENT_SECRET=

playground/app.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ const providers = computed(() =>
135135
disabled: Boolean(user.value?.dropbox),
136136
icon: 'i-simple-icons-dropbox',
137137
},
138+
{
139+
label: user.value?.polar || 'Polar',
140+
to: '/auth/polar',
141+
disabled: Boolean(user.value?.polar),
142+
icon: 'i-iconoir-polar-sh',
143+
},
138144
].map(p => ({
139145
...p,
140146
prefetch: false,

playground/auth.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ declare module '#auth-utils' {
2525
yandex?: string
2626
tiktok?: string
2727
dropbox?: string
28+
polar?: string
2829
}
2930

3031
interface UserSession {

playground/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
"generate": "nuxi generate"
99
},
1010
"dependencies": {
11+
"@iconify-json/gravity-ui": "^1.2.1",
12+
"@iconify-json/iconoir": "^1.2.1",
1113
"nuxt": "^3.13.2",
1214
"nuxt-auth-utils": "latest",
1315
"zod": "^3.23.8"
1416
},
1517
"devDependencies": {
16-
"@iconify-json/gravity-ui": "^1.2.1",
1718
"better-sqlite3": "^11.2.1"
1819
}
1920
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default defineOAuthPolarEventHandler({
2+
config: {
3+
emailRequired: true,
4+
},
5+
async onSuccess(event, { user }) {
6+
await setUserSession(event, {
7+
user: {
8+
polar: user.email,
9+
},
10+
loggedInAt: Date.now(),
11+
})
12+
13+
return sendRedirect(event, '/')
14+
},
15+
})

pnpm-lock.yaml

Lines changed: 13 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,5 +288,11 @@ export default defineNuxtModule<ModuleOptions>({
288288
clientSecret: '',
289289
redirectURL: '',
290290
})
291+
// Polar OAuth
292+
runtimeConfig.oauth.polar = defu(runtimeConfig.oauth.polar, {
293+
clientId: '',
294+
clientSecret: '',
295+
redirectURL: '',
296+
})
291297
},
292298
})

src/runtime/server/lib/oauth/polar.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import type { H3Event } from 'h3'
2+
import { eventHandler, getQuery, sendRedirect } from 'h3'
3+
import { withQuery } from 'ufo'
4+
import { defu } from 'defu'
5+
import { handleAccessTokenErrorResponse, handleMissingConfiguration, getOAuthRedirectURL, requestAccessToken } from '../utils'
6+
import { useRuntimeConfig, createError } from '#imports'
7+
import type { OAuthConfig } from '#auth-utils'
8+
9+
export interface OAuthPolarConfig {
10+
/**
11+
* Polar Client ID
12+
* @default process.env.NUXT_OAUTH_POLAR_CLIENT_ID
13+
*/
14+
clientId?: string
15+
16+
/**
17+
* Polar OAuth Client Secret
18+
* @default process.env.NUXT_OAUTH_POLAR_CLIENT_SECRET
19+
*/
20+
clientSecret?: string
21+
22+
/**
23+
* Polar OAuth Scope
24+
* @default []
25+
* @see https://api.polar.sh/.well-known/openid-configuration
26+
* @example ['openid']
27+
*/
28+
scope?: string[]
29+
30+
/**
31+
* Require email from user, adds the ['email'] scope if not present
32+
* @default false
33+
*/
34+
emailRequired?: boolean
35+
36+
/**
37+
* Polar OAuth Authorization URL
38+
* @see https://docs.polar.sh/api/authentication#start-the-authorization-flow
39+
* @default 'https://polar.sh/oauth2/authorize'
40+
*/
41+
authorizationURL?: string
42+
43+
/**
44+
* Polar OAuth Token URL
45+
* @see https://docs.polar.sh/api/authentication#exchange-the-authorization-code
46+
* @default 'https://api.polar.sh/v1/oauth2/token'
47+
*/
48+
tokenURL?: string
49+
50+
/**
51+
* Extra authorization parameters to provide to the authorization URL
52+
* @see https://docs.polar.sh/api/authentication#user-vs-organization-access-tokens
53+
* @example { sub_type: 'organization' }
54+
*/
55+
authorizationParams?: Record<string, string>
56+
57+
/**
58+
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
59+
* @default process.env.NUXT_OAUTH_POLAR_REDIRECT_URL or current URL
60+
*/
61+
redirectURL?: string
62+
}
63+
64+
export function defineOAuthPolarEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthPolarConfig>) {
65+
return eventHandler(async (event: H3Event) => {
66+
config = defu(config, useRuntimeConfig(event).oauth?.polar, {
67+
authorizationURL: 'https://polar.sh/oauth2/authorize',
68+
tokenURL: 'https://api.polar.sh/v1/oauth2/token',
69+
}) as OAuthPolarConfig
70+
const query = getQuery<{ code?: string }>(event)
71+
if (!config.clientId || !config.clientSecret) {
72+
return handleMissingConfiguration(event, 'polar', ['clientId', 'clientSecret'], onError)
73+
}
74+
75+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event)
76+
77+
if (!query.code) {
78+
config.scope = config.scope || []
79+
if (!config.scope.includes('openid'))
80+
config.scope.push('openid')
81+
if (config.emailRequired && !config.scope.includes('email'))
82+
config.scope.push('email')
83+
84+
// Redirect to Polar Oauth page
85+
return sendRedirect(
86+
event,
87+
withQuery(config.authorizationURL as string, {
88+
response_type: 'code',
89+
client_id: config.clientId,
90+
redirect_uri: redirectURL,
91+
scope: config.scope.join(' '),
92+
...config.authorizationParams,
93+
}),
94+
)
95+
}
96+
97+
const tokens = await requestAccessToken(config.tokenURL as string, {
98+
body: {
99+
grant_type: 'authorization_code',
100+
redirect_uri: redirectURL,
101+
client_id: config.clientId,
102+
client_secret: config.clientSecret,
103+
code: query.code,
104+
},
105+
})
106+
107+
if (tokens.error) {
108+
return handleAccessTokenErrorResponse(event, 'polar', tokens, onError)
109+
}
110+
const accessToken = tokens.access_token
111+
112+
// TODO: improve typing
113+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
114+
const user: any = await $fetch('https://api.polar.sh/v1/oauth2/userinfo', {
115+
headers: {
116+
Authorization: `Bearer ${accessToken}`,
117+
Accept: 'application/json',
118+
},
119+
})
120+
121+
if (!user) {
122+
const error = createError({
123+
statusCode: 500,
124+
message: 'Could not get Polar user',
125+
data: tokens,
126+
})
127+
if (!onError) throw error
128+
return onError(event, error)
129+
}
130+
131+
return onSuccess(event, {
132+
tokens,
133+
user,
134+
})
135+
})
136+
}

src/runtime/types/oauth-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { H3Event, H3Error } from 'h3'
22

3-
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 & {})
3+
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 & {})
44

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

0 commit comments

Comments
 (0)