Skip to content

Commit 520209c

Browse files
authored
feat: add LiveChat OAuth (#376)
1 parent 4ae262d commit 520209c

File tree

6 files changed

+192
-1
lines changed

6 files changed

+192
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ It can also be set using environment variables:
231231
- Line
232232
- Linear
233233
- LinkedIn
234+
- LiveChat
234235
- Microsoft
235236
- PayPal
236237
- Polar

playground/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,6 @@ NUXT_OAUTH_APPLE_KEY_ID=
118118
NUXT_OAUTH_APPLE_TEAM_ID=
119119
NUXT_OAUTH_APPLE_CLIENT_ID=
120120
NUXT_OAUTH_APPLE_REDIRECT_URL=
121+
#LiveChat
122+
NUXT_OAUTH_LIVECHAT_CLIENT_ID=
123+
NUXT_OAUTH_LIVECHAT_CLIENT_SECRET=
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export default defineOAuthLiveChatEventHandler({
2+
config: {},
3+
async onSuccess(event, { user }) {
4+
await setUserSession(event, {
5+
user: {
6+
livechat: user,
7+
},
8+
loggedInAt: Date.now(),
9+
})
10+
11+
return sendRedirect(event, '/')
12+
},
13+
async onError() {},
14+
})

src/module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,5 +424,10 @@ export default defineNuxtModule<ModuleOptions>({
424424
redirectURL: '',
425425
clientId: '',
426426
})
427+
// LiveChat OAuth
428+
runtimeConfig.oauth.livechat = defu(runtimeConfig.oauth.livechat, {
429+
clientId: '',
430+
clientSecret: '',
431+
})
427432
},
428433
})
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import type { H3Event } from 'h3'
2+
import { eventHandler, getQuery, sendRedirect } from 'h3'
3+
import { defu } from 'defu'
4+
import { withQuery } from 'ufo'
5+
import { randomUUID } from 'uncrypto'
6+
import {
7+
getOAuthRedirectURL,
8+
handleAccessTokenErrorResponse,
9+
handleMissingConfiguration,
10+
requestAccessToken,
11+
} from '../utils'
12+
import { useRuntimeConfig } from '#imports'
13+
import type { OAuthConfig } from '#auth-utils'
14+
15+
export interface LiveChatTokens {
16+
access_token: string
17+
account_id: string
18+
expires_in: number
19+
organization_id: string
20+
refresh_token: string
21+
scope: string
22+
token_type: string
23+
}
24+
25+
export interface LiveChatUser {
26+
account_id: string
27+
name: string
28+
email: string
29+
email_verified: boolean
30+
default_product: string | null
31+
default_organization_id: string | null
32+
avatar_url: string | null
33+
time_zone: string
34+
roles?: {
35+
role_id: string
36+
product: string
37+
role: string
38+
type: string
39+
predefined: boolean
40+
}[]
41+
updated_at: string
42+
created_at: string
43+
properties?: Record<string, unknown>
44+
}
45+
46+
export interface LiveChatConfig {
47+
/**
48+
* LiveChat OAuth Client ID
49+
* @default process.env.NUXT_LIVECHAT_CLIENT_ID
50+
*/
51+
clientId?: string
52+
53+
/**
54+
* LiveChat OAuth Client Secret
55+
* @default process.env.NUXT_LIVECHAT_CLIENT_SECRET
56+
*/
57+
clientSecret?: string
58+
59+
/**
60+
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
61+
* @default process.env.NUXT_OAUTH_LIVECHAT_REDIRECT_URL or current URL
62+
*/
63+
redirectURL?: string
64+
65+
/**
66+
* LiveChat OAuth Authorization URL
67+
* @default 'https://accounts.livechat.com
68+
*/
69+
authorizationURL?: string
70+
71+
/**
72+
* LiveChat OAuth Token URL
73+
* @default 'https://accounts.livechat.com/v2/token
74+
*/
75+
tokenURL?: string
76+
77+
/**
78+
* LiveChat User URL
79+
* @default 'https://accounts.livechat.com/v2/accounts/me
80+
*/
81+
userURL?: string
82+
83+
/**
84+
* LiveChat OAuth Scope. accounts--my:ro is always applied to get user profile.
85+
* @default ['accounts--my:ro']
86+
* @example ['accounts--my:ro', 'chats--my:ro']
87+
*/
88+
scope?: string[]
89+
90+
/**
91+
* Extra authorization parameters to provide to the authorization URL
92+
* @see https://platform.text.com/docs/authorization/authorization-in-practice
93+
*/
94+
authorizationParams?: Record<string, string>
95+
}
96+
97+
export function defineOAuthLiveChatEventHandler({
98+
config,
99+
onSuccess,
100+
onError,
101+
}: OAuthConfig<LiveChatConfig>) {
102+
return eventHandler(async (event: H3Event) => {
103+
config = defu(config, useRuntimeConfig(event).oauth?.livechat, {
104+
authorizationURL: 'https://accounts.livechat.com',
105+
tokenURL: 'https://accounts.livechat.com/v2/token',
106+
userURL: 'https://accounts.livechat.com/v2/accounts/me',
107+
scope: [],
108+
authorizationParams: {
109+
state: randomUUID(),
110+
},
111+
}) as LiveChatConfig
112+
113+
if (!config.clientId || !config.clientSecret) {
114+
return handleMissingConfiguration(
115+
event,
116+
'livechat',
117+
['clientId', 'clientSecret'],
118+
onError,
119+
)
120+
}
121+
122+
const query = getQuery<{ code?: string }>(event)
123+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event)
124+
125+
// Ensure accounts--my:ro is always applied.
126+
const scope = [...new Set([...config.scope!, 'accounts--my:ro'])].join(' ')
127+
128+
if (!query.code) {
129+
return sendRedirect(
130+
event,
131+
withQuery(config.authorizationURL!, {
132+
client_id: config.clientId,
133+
redirect_uri: redirectURL,
134+
response_type: 'code',
135+
scope,
136+
...config.authorizationParams,
137+
}),
138+
)
139+
}
140+
141+
const tokens = await requestAccessToken(config.tokenURL!, {
142+
params: {
143+
grant_type: 'authorization_code',
144+
code: query.code,
145+
client_id: config.clientId,
146+
client_secret: config.clientSecret,
147+
redirect_uri: redirectURL,
148+
},
149+
}).catch((error) => {
150+
return { error }
151+
})
152+
153+
if (tokens.error) {
154+
return handleAccessTokenErrorResponse(event, 'livechat', tokens, onError)
155+
}
156+
157+
const user = await $fetch<LiveChatUser>(config.userURL!, {
158+
headers: {
159+
Authorization: `Bearer ${tokens.access_token}`,
160+
},
161+
})
162+
163+
return onSuccess(event, {
164+
tokens,
165+
user,
166+
})
167+
})
168+
}

src/runtime/types/oauth-config.ts

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

33
export type ATProtoProvider = 'bluesky'
44

5-
export type OAuthProvider = ATProtoProvider | 'atlassian' | 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'gitea' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'keycloak' | 'line' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | 'apple' | (string & {})
5+
export type OAuthProvider = ATProtoProvider | 'atlassian' | 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'gitea' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'keycloak' | 'line' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | 'apple' | 'livechat' | (string & {})
66

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

0 commit comments

Comments
 (0)