Add secure support connection to website (#5750)

* wip: icom jwts

* should fix auth token passing

* add to wrangler
This commit is contained in:
aecsocket
2026-04-04 16:47:03 +01:00
committed by GitHub
parent 3091021194
commit bb3506823d
7 changed files with 149 additions and 29 deletions

View File

@@ -207,10 +207,19 @@ export default defineNuxtConfig({
// @ts-ignore // @ts-ignore
rateLimitKey: process.env.RATE_LIMIT_IGNORE_KEY ?? globalThis.RATE_LIMIT_IGNORE_KEY, rateLimitKey: process.env.RATE_LIMIT_IGNORE_KEY ?? globalThis.RATE_LIMIT_IGNORE_KEY,
pyroBaseUrl: process.env.PYRO_BASE_URL, pyroBaseUrl: process.env.PYRO_BASE_URL,
intercomIdentitySecret:
process.env.INTERCOM_IDENTITY_SECRET ||
// @ts-ignore
globalThis.INTERCOM_IDENTITY_SECRET,
public: { public: {
apiBaseUrl: getApiUrl(), apiBaseUrl: getApiUrl(),
pyroBaseUrl: process.env.PYRO_BASE_URL, pyroBaseUrl: process.env.PYRO_BASE_URL,
siteUrl: getDomain(), siteUrl: getDomain(),
intercomAppId:
process.env.INTERCOM_APP_ID ||
// @ts-ignore
globalThis.INTERCOM_APP_ID ||
'ykeritl9',
production: isProduction(), production: isProduction(),
buildEnv: process.env.BUILD_ENV, buildEnv: process.env.BUILD_ENV,
preview: process.env.PREVIEW === 'true', preview: process.env.PREVIEW === 'true',

View File

@@ -63,6 +63,7 @@
"highlight.js": "^11.7.0", "highlight.js": "^11.7.0",
"intl-messageformat": "^10.7.7", "intl-messageformat": "^10.7.7",
"iso-3166-2": "1.0.0", "iso-3166-2": "1.0.0",
"jose": "^6.2.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lru-cache": "^11.2.4", "lru-cache": "^11.2.4",

View File

@@ -414,17 +414,16 @@ const isLoading = ref(true)
const isMounted = ref(true) const isMounted = ref(true)
const unsubscribers = ref<(() => void)[]>([]) const unsubscribers = ref<(() => void)[]>([])
const flags = useFeatureFlags() const flags = useFeatureFlags()
const config = useRuntimeConfig()
const INTERCOM_APP_ID = ref('ykeritl9') type AuthUser = {
const auth = (await useAuth()) as unknown as { id: string
value: { user: { id: string; username: string; email: string; created: string } } username: string
email?: string
created: string
} }
const userId = ref(auth.value?.user?.id ?? null)
const username = ref(auth.value?.user?.username ?? null) const auth = (await useAuth()) as unknown as { value: { user: AuthUser | null } }
const email = ref(auth.value?.user?.email ?? null)
const createdAt = ref(
auth.value?.user?.created ? Math.floor(new Date(auth.value.user.created).getTime() / 1000) : null,
)
const debug = useDebugLogger('ServerManage') const debug = useDebugLogger('ServerManage')
const route = useNativeRoute() const route = useNativeRoute()
@@ -1332,6 +1331,22 @@ const openInstallLog = () => {
}) })
} }
async function initializeIntercom() {
if (!auth.value?.user) return
try {
const intercomData = await $fetch<{ token: string }>('/api/intercom/messenger-jwt')
Intercom({
app_id: config.public.intercomAppId,
intercom_user_jwt: intercomData.token,
session_duration: 1000 * 60 * 60 * 24,
})
} catch (error) {
console.warn('[PYROSERVERS][INTERCOM] failed to initialize secure support chat', error)
}
}
const cleanup = () => { const cleanup = () => {
isMounted.value = false isMounted.value = false
@@ -1490,26 +1505,7 @@ onMounted(() => {
}) })
} }
if (username.value && email.value && userId.value && createdAt.value) { void initializeIntercom()
const currentUser = auth.value?.user as any
const matches =
username.value === currentUser?.username &&
email.value === currentUser?.email &&
userId.value === currentUser?.id &&
createdAt.value === Math.floor(new Date(currentUser?.created).getTime() / 1000)
if (matches) {
Intercom({
app_id: INTERCOM_APP_ID.value,
userId: userId.value,
name: username.value,
email: email.value,
created_at: createdAt.value,
})
} else {
console.warn('[PYROSERVERS][INTERCOM] mismatch')
}
}
DOMPurify.addHook( DOMPurify.addHook(
'afterSanitizeAttributes', 'afterSanitizeAttributes',

View File

@@ -0,0 +1,99 @@
import { type Labrinth, ModrinthApiError } from '@modrinth/api-client'
import { SignJWT } from 'jose'
import { useServerModrinthClient } from '~/server/utils/api-client'
type IntercomTokenResponse = {
token: string
}
async function signIntercomUserJwt(
user: { id: string; username: string; email?: string; created: string },
secret: string,
): Promise<string> {
const createdAt = Math.floor(new Date(user.created).getTime() / 1000)
const payload: Record<string, string | number> = {
user_id: user.id,
name: user.username,
}
if (user.email) {
payload.email = user.email
}
if (Number.isFinite(createdAt)) {
payload.created_at = createdAt
}
return await new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
.setIssuedAt()
.setExpirationTime('1h')
.sign(new TextEncoder().encode(secret))
}
export default defineEventHandler(async (event): Promise<IntercomTokenResponse> => {
if (event.method !== 'GET') {
throw createError({
statusCode: 405,
message: 'Method not allowed',
})
}
const authToken = getCookie(event, 'auth-token')
if (!authToken) {
throw createError({
statusCode: 401,
message: 'Authentication required',
})
}
setHeader(event, 'cache-control', 'private, no-store, max-age=0')
const config = useRuntimeConfig(event)
if (!config.intercomIdentitySecret) {
throw createError({
statusCode: 500,
message: 'Intercom identity secret is not configured',
})
}
const client = useServerModrinthClient({
event,
authToken,
})
let user: { id: string; username: string; email?: string; created: string }
try {
const currentUser = await client.request<Labrinth.Users.v2.User>('/user', {
api: 'labrinth',
version: 2,
method: 'GET',
})
user = {
id: currentUser.id,
username: currentUser.username,
email: currentUser.email,
created: currentUser.created,
}
} catch (error) {
if (error instanceof ModrinthApiError && error.statusCode === 401) {
throw createError({
statusCode: 401,
message: 'Authentication required',
})
}
throw createError({
statusCode: 502,
message: 'Failed to resolve current user',
})
}
const token = await signIntercomUserJwt(user, config.intercomIdentitySecret)
return {
token,
}
})

View File

@@ -24,6 +24,11 @@
"binding": "RATE_LIMIT_IGNORE_KEY", "binding": "RATE_LIMIT_IGNORE_KEY",
"store_id": "c9024fef252d4a53adf513feca64417d", "store_id": "c9024fef252d4a53adf513feca64417d",
"secret_name": "labrinth-production-ratelimit-key" "secret_name": "labrinth-production-ratelimit-key"
},
{
"binding": "INTERCOM_IDENTITY_SECRET",
"store_id": "c9024fef252d4a53adf513feca64417d",
"secret_name": "intercom-identity-secret"
} }
], ],
"version_metadata": { "version_metadata": {

8
pnpm-lock.yaml generated
View File

@@ -326,6 +326,9 @@ importers:
iso-3166-2: iso-3166-2:
specifier: 1.0.0 specifier: 1.0.0
version: 1.0.0 version: 1.0.0
jose:
specifier: ^6.2.2
version: 6.2.2
js-yaml: js-yaml:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.1 version: 4.1.1
@@ -6727,6 +6730,9 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true hasBin: true
jose@6.2.2:
resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
js-beautify@1.15.4: js-beautify@1.15.4:
resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -16355,6 +16361,8 @@ snapshots:
jiti@2.6.1: {} jiti@2.6.1: {}
jose@6.2.2: {}
js-beautify@1.15.4: js-beautify@1.15.4:
dependencies: dependencies:
config-chain: 1.1.13 config-chain: 1.1.13

View File

@@ -24,6 +24,8 @@
"CF_PAGES_*", "CF_PAGES_*",
"HEROKU_APP_NAME", "HEROKU_APP_NAME",
"STRIPE_PUBLISHABLE_KEY", "STRIPE_PUBLISHABLE_KEY",
"INTERCOM_APP_ID",
"INTERCOM_IDENTITY_SECRET",
"PYRO_BASE_URL", "PYRO_BASE_URL",
"PROD_OVERRIDE", "PROD_OVERRIDE",
"PYRO_MASTER_KEY", "PYRO_MASTER_KEY",