Skip to content

Commit bb35068

Browse files
authored
Add secure support connection to website (#5750)
* wip: icom jwts * should fix auth token passing * add to wrangler
1 parent 3091021 commit bb35068

7 files changed

Lines changed: 149 additions & 29 deletions

File tree

apps/frontend/nuxt.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,19 @@ export default defineNuxtConfig({
207207
// @ts-ignore
208208
rateLimitKey: process.env.RATE_LIMIT_IGNORE_KEY ?? globalThis.RATE_LIMIT_IGNORE_KEY,
209209
pyroBaseUrl: process.env.PYRO_BASE_URL,
210+
intercomIdentitySecret:
211+
process.env.INTERCOM_IDENTITY_SECRET ||
212+
// @ts-ignore
213+
globalThis.INTERCOM_IDENTITY_SECRET,
210214
public: {
211215
apiBaseUrl: getApiUrl(),
212216
pyroBaseUrl: process.env.PYRO_BASE_URL,
213217
siteUrl: getDomain(),
218+
intercomAppId:
219+
process.env.INTERCOM_APP_ID ||
220+
// @ts-ignore
221+
globalThis.INTERCOM_APP_ID ||
222+
'ykeritl9',
214223
production: isProduction(),
215224
buildEnv: process.env.BUILD_ENV,
216225
preview: process.env.PREVIEW === 'true',

apps/frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"highlight.js": "^11.7.0",
6464
"intl-messageformat": "^10.7.7",
6565
"iso-3166-2": "1.0.0",
66+
"jose": "^6.2.2",
6667
"js-yaml": "^4.1.0",
6768
"jszip": "^3.10.1",
6869
"lru-cache": "^11.2.4",

apps/frontend/src/pages/hosting/manage/[id].vue

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -414,17 +414,16 @@ const isLoading = ref(true)
414414
const isMounted = ref(true)
415415
const unsubscribers = ref<(() => void)[]>([])
416416
const flags = useFeatureFlags()
417+
const config = useRuntimeConfig()
417418
418-
const INTERCOM_APP_ID = ref('ykeritl9')
419-
const auth = (await useAuth()) as unknown as {
420-
value: { user: { id: string; username: string; email: string; created: string } }
419+
type AuthUser = {
420+
id: string
421+
username: string
422+
email?: string
423+
created: string
421424
}
422-
const userId = ref(auth.value?.user?.id ?? null)
423-
const username = ref(auth.value?.user?.username ?? null)
424-
const email = ref(auth.value?.user?.email ?? null)
425-
const createdAt = ref(
426-
auth.value?.user?.created ? Math.floor(new Date(auth.value.user.created).getTime() / 1000) : null,
427-
)
425+
426+
const auth = (await useAuth()) as unknown as { value: { user: AuthUser | null } }
428427
429428
const debug = useDebugLogger('ServerManage')
430429
const route = useNativeRoute()
@@ -1332,6 +1331,22 @@ const openInstallLog = () => {
13321331
})
13331332
}
13341333
1334+
async function initializeIntercom() {
1335+
if (!auth.value?.user) return
1336+
1337+
try {
1338+
const intercomData = await $fetch<{ token: string }>('/api/intercom/messenger-jwt')
1339+
1340+
Intercom({
1341+
app_id: config.public.intercomAppId,
1342+
intercom_user_jwt: intercomData.token,
1343+
session_duration: 1000 * 60 * 60 * 24,
1344+
})
1345+
} catch (error) {
1346+
console.warn('[PYROSERVERS][INTERCOM] failed to initialize secure support chat', error)
1347+
}
1348+
}
1349+
13351350
const cleanup = () => {
13361351
isMounted.value = false
13371352
@@ -1490,26 +1505,7 @@ onMounted(() => {
14901505
})
14911506
}
14921507
1493-
if (username.value && email.value && userId.value && createdAt.value) {
1494-
const currentUser = auth.value?.user as any
1495-
const matches =
1496-
username.value === currentUser?.username &&
1497-
email.value === currentUser?.email &&
1498-
userId.value === currentUser?.id &&
1499-
createdAt.value === Math.floor(new Date(currentUser?.created).getTime() / 1000)
1500-
1501-
if (matches) {
1502-
Intercom({
1503-
app_id: INTERCOM_APP_ID.value,
1504-
userId: userId.value,
1505-
name: username.value,
1506-
email: email.value,
1507-
created_at: createdAt.value,
1508-
})
1509-
} else {
1510-
console.warn('[PYROSERVERS][INTERCOM] mismatch')
1511-
}
1512-
}
1508+
void initializeIntercom()
15131509
15141510
DOMPurify.addHook(
15151511
'afterSanitizeAttributes',
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { type Labrinth, ModrinthApiError } from '@modrinth/api-client'
2+
import { SignJWT } from 'jose'
3+
4+
import { useServerModrinthClient } from '~/server/utils/api-client'
5+
6+
type IntercomTokenResponse = {
7+
token: string
8+
}
9+
10+
async function signIntercomUserJwt(
11+
user: { id: string; username: string; email?: string; created: string },
12+
secret: string,
13+
): Promise<string> {
14+
const createdAt = Math.floor(new Date(user.created).getTime() / 1000)
15+
16+
const payload: Record<string, string | number> = {
17+
user_id: user.id,
18+
name: user.username,
19+
}
20+
21+
if (user.email) {
22+
payload.email = user.email
23+
}
24+
25+
if (Number.isFinite(createdAt)) {
26+
payload.created_at = createdAt
27+
}
28+
29+
return await new SignJWT(payload)
30+
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
31+
.setIssuedAt()
32+
.setExpirationTime('1h')
33+
.sign(new TextEncoder().encode(secret))
34+
}
35+
36+
export default defineEventHandler(async (event): Promise<IntercomTokenResponse> => {
37+
if (event.method !== 'GET') {
38+
throw createError({
39+
statusCode: 405,
40+
message: 'Method not allowed',
41+
})
42+
}
43+
44+
const authToken = getCookie(event, 'auth-token')
45+
if (!authToken) {
46+
throw createError({
47+
statusCode: 401,
48+
message: 'Authentication required',
49+
})
50+
}
51+
52+
setHeader(event, 'cache-control', 'private, no-store, max-age=0')
53+
54+
const config = useRuntimeConfig(event)
55+
if (!config.intercomIdentitySecret) {
56+
throw createError({
57+
statusCode: 500,
58+
message: 'Intercom identity secret is not configured',
59+
})
60+
}
61+
62+
const client = useServerModrinthClient({
63+
event,
64+
authToken,
65+
})
66+
67+
let user: { id: string; username: string; email?: string; created: string }
68+
try {
69+
const currentUser = await client.request<Labrinth.Users.v2.User>('/user', {
70+
api: 'labrinth',
71+
version: 2,
72+
method: 'GET',
73+
})
74+
user = {
75+
id: currentUser.id,
76+
username: currentUser.username,
77+
email: currentUser.email,
78+
created: currentUser.created,
79+
}
80+
} catch (error) {
81+
if (error instanceof ModrinthApiError && error.statusCode === 401) {
82+
throw createError({
83+
statusCode: 401,
84+
message: 'Authentication required',
85+
})
86+
}
87+
88+
throw createError({
89+
statusCode: 502,
90+
message: 'Failed to resolve current user',
91+
})
92+
}
93+
94+
const token = await signIntercomUserJwt(user, config.intercomIdentitySecret)
95+
96+
return {
97+
token,
98+
}
99+
})

apps/frontend/wrangler.jsonc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
"binding": "RATE_LIMIT_IGNORE_KEY",
2525
"store_id": "c9024fef252d4a53adf513feca64417d",
2626
"secret_name": "labrinth-production-ratelimit-key"
27+
},
28+
{
29+
"binding": "INTERCOM_IDENTITY_SECRET",
30+
"store_id": "c9024fef252d4a53adf513feca64417d",
31+
"secret_name": "intercom-identity-secret"
2732
}
2833
],
2934
"version_metadata": {

pnpm-lock.yaml

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

turbo.jsonc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
"CF_PAGES_*",
2525
"HEROKU_APP_NAME",
2626
"STRIPE_PUBLISHABLE_KEY",
27+
"INTERCOM_APP_ID",
28+
"INTERCOM_IDENTITY_SECRET",
2729
"PYRO_BASE_URL",
2830
"PROD_OVERRIDE",
2931
"PYRO_MASTER_KEY",

0 commit comments

Comments
 (0)