mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 11:36:05 +00:00
wip: icom jwts
This commit is contained in:
@@ -207,10 +207,19 @@ export default defineNuxtConfig({
|
||||
// @ts-ignore
|
||||
rateLimitKey: process.env.RATE_LIMIT_IGNORE_KEY ?? globalThis.RATE_LIMIT_IGNORE_KEY,
|
||||
pyroBaseUrl: process.env.PYRO_BASE_URL,
|
||||
intercomIdentitySecret:
|
||||
process.env.INTERCOM_IDENTITY_SECRET ||
|
||||
// @ts-ignore
|
||||
globalThis.INTERCOM_IDENTITY_SECRET,
|
||||
public: {
|
||||
apiBaseUrl: getApiUrl(),
|
||||
pyroBaseUrl: process.env.PYRO_BASE_URL,
|
||||
siteUrl: getDomain(),
|
||||
intercomAppId:
|
||||
process.env.INTERCOM_APP_ID ||
|
||||
// @ts-ignore
|
||||
globalThis.INTERCOM_APP_ID ||
|
||||
'ykeritl9',
|
||||
production: isProduction(),
|
||||
buildEnv: process.env.BUILD_ENV,
|
||||
preview: process.env.PREVIEW === 'true',
|
||||
|
||||
@@ -414,17 +414,16 @@ const isLoading = ref(true)
|
||||
const isMounted = ref(true)
|
||||
const unsubscribers = ref<(() => void)[]>([])
|
||||
const flags = useFeatureFlags()
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const INTERCOM_APP_ID = ref('ykeritl9')
|
||||
const auth = (await useAuth()) as unknown as {
|
||||
value: { user: { id: string; username: string; email: string; created: string } }
|
||||
type AuthUser = {
|
||||
id: string
|
||||
username: string
|
||||
email?: string
|
||||
created: string
|
||||
}
|
||||
const userId = ref(auth.value?.user?.id ?? null)
|
||||
const username = ref(auth.value?.user?.username ?? 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 auth = (await useAuth()) as unknown as { value: { user: AuthUser | null } }
|
||||
|
||||
const debug = useDebugLogger('ServerManage')
|
||||
const route = useNativeRoute()
|
||||
@@ -1332,6 +1331,29 @@ const openInstallLog = () => {
|
||||
})
|
||||
}
|
||||
|
||||
async function initializeIntercom() {
|
||||
if (!auth.value?.user) return
|
||||
|
||||
try {
|
||||
const intercomData = await $fetch<{
|
||||
token: string
|
||||
user: AuthUser
|
||||
}>('/api/intercom/messenger-jwt')
|
||||
|
||||
Intercom({
|
||||
app_id: config.public.intercomAppId,
|
||||
intercom_user_jwt: intercomData.token,
|
||||
user_id: intercomData.user.id,
|
||||
name: intercomData.user.username,
|
||||
email: intercomData.user.email,
|
||||
created_at: Math.floor(new Date(intercomData.user.created).getTime() / 1000),
|
||||
session_duration: 1000 * 60 * 60 * 24,
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('[PYROSERVERS][INTERCOM] failed to initialize secure support chat', error)
|
||||
}
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
isMounted.value = false
|
||||
|
||||
@@ -1490,26 +1512,7 @@ onMounted(() => {
|
||||
})
|
||||
}
|
||||
|
||||
if (username.value && email.value && userId.value && createdAt.value) {
|
||||
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')
|
||||
}
|
||||
}
|
||||
void initializeIntercom()
|
||||
|
||||
DOMPurify.addHook(
|
||||
'afterSanitizeAttributes',
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { createHmac } from 'node:crypto'
|
||||
|
||||
import { ModrinthApiError } from '@modrinth/api-client'
|
||||
|
||||
import { useServerModrinthClient } from '~/server/utils/api-client'
|
||||
|
||||
type AuthenticatedUser = {
|
||||
id: string
|
||||
username: string
|
||||
email?: string
|
||||
created: string
|
||||
}
|
||||
|
||||
type IntercomTokenResponse = {
|
||||
token: string
|
||||
user: AuthenticatedUser
|
||||
}
|
||||
|
||||
function base64UrlEncode(input: string | Buffer): string {
|
||||
return Buffer.from(input)
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/g, '')
|
||||
}
|
||||
|
||||
function signIntercomUserJwt(payload: Record<string, unknown>, secret: string): string {
|
||||
const header = {
|
||||
alg: 'HS256',
|
||||
typ: 'JWT',
|
||||
}
|
||||
|
||||
const encodedHeader = base64UrlEncode(JSON.stringify(header))
|
||||
const encodedPayload = base64UrlEncode(JSON.stringify(payload))
|
||||
const unsignedToken = `${encodedHeader}.${encodedPayload}`
|
||||
|
||||
const signature = createHmac('sha256', secret).update(unsignedToken).digest()
|
||||
|
||||
return `${unsignedToken}.${base64UrlEncode(signature)}`
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event): Promise<IntercomTokenResponse> => {
|
||||
if (getMethod(event) !== 'GET') {
|
||||
throw createError({
|
||||
statusCode: 405,
|
||||
message: 'Method not allowed',
|
||||
})
|
||||
}
|
||||
|
||||
const headerToken = getHeader(event, 'authorization')
|
||||
const parsedHeaderToken = headerToken?.replace(/^Bearer\s+/i, '').trim()
|
||||
const authToken = parsedHeaderToken || 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: AuthenticatedUser
|
||||
try {
|
||||
const currentUser = await client.request<AuthenticatedUser>('/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 now = Math.floor(Date.now() / 1000)
|
||||
const token = signIntercomUserJwt(
|
||||
{
|
||||
user_id: user.id,
|
||||
email: user.email,
|
||||
name: user.username,
|
||||
created_at: Math.floor(new Date(user.created).getTime() / 1000),
|
||||
iat: now,
|
||||
exp: now + 60 * 60,
|
||||
},
|
||||
config.intercomIdentitySecret,
|
||||
)
|
||||
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
}
|
||||
})
|
||||
@@ -24,6 +24,8 @@
|
||||
"CF_PAGES_*",
|
||||
"HEROKU_APP_NAME",
|
||||
"STRIPE_PUBLISHABLE_KEY",
|
||||
"INTERCOM_APP_ID",
|
||||
"INTERCOM_IDENTITY_SECRET",
|
||||
"PYRO_BASE_URL",
|
||||
"PROD_OVERRIDE",
|
||||
"PYRO_MASTER_KEY",
|
||||
|
||||
Reference in New Issue
Block a user