Skip to content

Commit 1a79baf

Browse files
feat: add hubspot provider
* feat: add hubspot to the docs * feat: add hubspot to playground * feat: add hubspot module config * feat: add hubspot to runtime config * feat: add auth handle * fix: provider name * feat: add types * fix: scope * fix: use spaces for scopes separation
1 parent 3532d48 commit 1a79baf

File tree

8 files changed

+155
-1
lines changed

8 files changed

+155
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ It can also be set using environment variables:
214214
- GitHub
215215
- GitLab
216216
- Google
217+
- Hubspot
217218
- Instagram
218219
- Keycloak
219220
- Linear

playground/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,7 @@ NUXT_OAUTH_AUTHENTIK_DOMAIN=
9797
# Strava
9898
NUXT_OAUTH_STRAVA_CLIENT_ID=
9999
NUXT_OAUTH_STRAVA_CLIENT_SECRET=
100+
# Hubspot
101+
NUXT_OAUTH_HUBSPOT_CLIENT_ID=
102+
NUXT_OAUTH_HUBSPOT_CLIENT_SECRET=
103+
NUXT_OAUTH_HUBSPOT_REDIRECT_URL=

playground/app.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ const providers = computed(() =>
177177
disabled: Boolean(user.value?.strava),
178178
icon: 'i-simple-icons-strava',
179179
},
180+
{
181+
label: user.value?.hubspot || 'HubSpot',
182+
to: '/auth/hubspot',
183+
disabled: Boolean(user.value?.hubspot),
184+
icon: 'i-simple-icons-hubspot',
185+
},
180186
].map(p => ({
181187
...p,
182188
prefetch: false,

playground/auth.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ declare module '#auth-utils' {
3232
authentik?: string
3333
seznam?: string
3434
strava?: string
35+
hubspot?: string
3536
}
3637

3738
interface UserSession {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default defineOAuthHubspotEventHandler({
2+
config: {
3+
scope: ['oauth'],
4+
},
5+
async onSuccess(event, { user }) {
6+
await setUserSession(event, {
7+
user: {
8+
hubspot: user.email,
9+
},
10+
loggedInAt: Date.now(),
11+
})
12+
13+
return sendRedirect(event, '/')
14+
},
15+
})

src/module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,5 +348,11 @@ export default defineNuxtModule<ModuleOptions>({
348348
clientSecret: '',
349349
redirectURL: '',
350350
})
351+
// Hubspot OAuth
352+
runtimeConfig.oauth.hubspot = defu(runtimeConfig.oauth.hubspot, {
353+
clientId: '',
354+
clientSecret: '',
355+
redirectURL: '',
356+
})
351357
},
352358
})
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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 {
6+
getOAuthRedirectURL,
7+
handleAccessTokenErrorResponse,
8+
handleMissingConfiguration,
9+
requestAccessToken,
10+
} from '../utils'
11+
import { useRuntimeConfig } from '#imports'
12+
import type { OAuthConfig } from '#auth-utils'
13+
14+
export interface OAuthHubspotConfig {
15+
/**
16+
* Hubspot OAuth Client ID
17+
* @default process.env.NUXT_OAUTH_HUBSPOT_CLIENT_ID
18+
*/
19+
clientId?: string
20+
21+
/**
22+
* Hubspot OAuth Client Secret
23+
* @default process.env.NUXT_OAUTH_HUBSPOT_CLIENT_SECRET
24+
*/
25+
clientSecret?: string
26+
27+
/**
28+
* Hubspot OAuth Redirect URL
29+
* @default process.env.NUXT_OAUTH_HUBSPOT_REDIRECT_URL
30+
*/
31+
redirectURL?: string
32+
33+
/**
34+
* Hubspot OAuth Scope
35+
* @default ['oauth']
36+
* @see https://developers.hubspot.com/beta-docs/guides/apps/authentication/scopes
37+
* @example ['accounting', 'automation', 'actions']
38+
*/
39+
scope?: string[]
40+
}
41+
interface SignedAccessToken {
42+
expiresAt: number
43+
scopes: string
44+
hubId: number
45+
userId: number
46+
appId: number
47+
signature: string
48+
scopeToScopeGroupPks?: string
49+
newSignature?: string
50+
hublet?: string
51+
trialScopes?: string
52+
trialScopeToScopeGroupPks?: string
53+
isUserLevel: boolean
54+
}
55+
56+
interface OAuthHubspotAccessInfo {
57+
token: string
58+
user: string
59+
hub_domain: string
60+
scopes: string[]
61+
signed_access_token: SignedAccessToken
62+
hub_id: number
63+
app_id: number
64+
expires_in: number
65+
user_id: number
66+
token_type: string
67+
}
68+
69+
export function defineOAuthHubspotEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthHubspotConfig>) {
70+
return eventHandler(async (event: H3Event) => {
71+
config = defu(config, useRuntimeConfig(event).oauth?.hubspot) as OAuthHubspotConfig
72+
73+
if (!config.clientId || !config.clientSecret || !config.redirectURL) {
74+
return handleMissingConfiguration(event, 'hubspot', ['clientId', 'clientSecret', 'redirectURL'], onError)
75+
}
76+
77+
const query = getQuery<{ code?: string, state?: string, error?: string, error_description?: string }>(event)
78+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event)
79+
80+
if (query.error) {
81+
return handleAccessTokenErrorResponse(event, 'hubspot', query, onError)
82+
}
83+
84+
if (!query.code) {
85+
return sendRedirect(
86+
event,
87+
withQuery('https://app.hubspot.com/oauth/authorize', {
88+
client_id: config.clientId,
89+
redirect_uri: redirectURL,
90+
scope: config.scope?.join(' ') || 'oauth',
91+
}),
92+
)
93+
}
94+
95+
const tokens = await requestAccessToken(
96+
'https://api.hubapi.com/oauth/v1/token', {
97+
body: {
98+
client_id: config.clientId,
99+
client_secret: config.clientSecret,
100+
code: query.code as string,
101+
redirect_uri: redirectURL,
102+
grant_type: 'authorization_code',
103+
},
104+
})
105+
106+
if (tokens.error) {
107+
return handleAccessTokenErrorResponse(event, 'hubspot', tokens, onError)
108+
}
109+
110+
const info: OAuthHubspotAccessInfo = await $fetch('https://api.hubapi.com/oauth/v1/access-tokens/' + tokens.access_token)
111+
112+
return onSuccess(event, {
113+
user: {
114+
id: info.user_id,
115+
email: info.user,
116+
domain: info.hub_domain,
117+
},
118+
tokens,
119+
})
120+
})
121+
}

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' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'github' | 'gitlab' | 'google' | 'instagram' | 'keycloak' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | (string & {})
3+
export type OAuthProvider = 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'keycloak' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | (string & {})
44

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

0 commit comments

Comments
 (0)