456d9398a1
* Load bets and comments tabs data on user page independently * Implement basic pagination on profile comments list * Tweak server auth to return `null` instead of `undefined` * Switch to SSR for user page * Fix lint * Fix broken contract fetching in user bets list * Tidying
214 lines
6.5 KiB
TypeScript
214 lines
6.5 KiB
TypeScript
import fetch from 'node-fetch'
|
|
import { IncomingMessage, ServerResponse } from 'http'
|
|
import { FIREBASE_CONFIG, PROJECT_ID } from 'common/envs/constants'
|
|
import { getFunctionUrl } from 'common/api'
|
|
import { UserCredential } from 'firebase/auth'
|
|
import {
|
|
getTokensFromCookies,
|
|
setTokenCookies,
|
|
deleteTokenCookies,
|
|
} from './auth'
|
|
import {
|
|
GetServerSideProps,
|
|
GetServerSidePropsContext,
|
|
GetServerSidePropsResult,
|
|
} from 'next'
|
|
|
|
// server firebase SDK
|
|
import * as admin from 'firebase-admin'
|
|
|
|
// client firebase SDK
|
|
import { app as clientApp } from './init'
|
|
import { getAuth, signInWithCustomToken } from 'firebase/auth'
|
|
|
|
const ensureApp = async () => {
|
|
// Note: firebase-admin can only be imported from a server context,
|
|
// because it relies on Node standard library dependencies.
|
|
if (admin.apps.length === 0) {
|
|
// never initialize twice
|
|
return admin.initializeApp({ projectId: PROJECT_ID })
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
return admin.apps[0]!
|
|
}
|
|
|
|
const requestFirebaseIdToken = async (refreshToken: string) => {
|
|
// See https://firebase.google.com/docs/reference/rest/auth/#section-refresh-token
|
|
const refreshUrl = new URL('https://securetoken.googleapis.com/v1/token')
|
|
refreshUrl.searchParams.append('key', FIREBASE_CONFIG.apiKey)
|
|
const result = await fetch(refreshUrl.toString(), {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: new URLSearchParams({
|
|
grant_type: 'refresh_token',
|
|
refresh_token: refreshToken,
|
|
}),
|
|
})
|
|
if (!result.ok) {
|
|
throw new Error(`Could not refresh ID token: ${await result.text()}`)
|
|
}
|
|
return (await result.json()) as { id_token: string; refresh_token: string }
|
|
}
|
|
|
|
const requestManifoldCustomToken = async (idToken: string) => {
|
|
const functionUrl = getFunctionUrl('getcustomtoken')
|
|
const result = await fetch(functionUrl, {
|
|
method: 'GET',
|
|
headers: {
|
|
Authorization: `Bearer ${idToken}`,
|
|
},
|
|
})
|
|
if (!result.ok) {
|
|
throw new Error(`Could not get custom token: ${await result.text()}`)
|
|
}
|
|
return (await result.json()) as { token: string }
|
|
}
|
|
|
|
type RequestContext = {
|
|
req: IncomingMessage
|
|
res: ServerResponse
|
|
}
|
|
|
|
const authAndRefreshTokens = async (ctx: RequestContext) => {
|
|
const adminAuth = (await ensureApp()).auth()
|
|
const clientAuth = getAuth(clientApp)
|
|
console.debug('Initialized Firebase auth libraries.')
|
|
|
|
let { id, refresh, custom } = getTokensFromCookies(ctx.req)
|
|
|
|
// step 0: if you have no refresh token you are logged out
|
|
if (refresh == null) {
|
|
console.debug('User is unauthenticated.')
|
|
return null
|
|
}
|
|
|
|
console.debug('User may be authenticated; checking cookies.')
|
|
|
|
// step 1: given a valid refresh token, ensure a valid ID token
|
|
if (id != null) {
|
|
// if they have an ID token, throw it out if it's invalid/expired
|
|
try {
|
|
await adminAuth.verifyIdToken(id)
|
|
console.debug('Verified ID token.')
|
|
} catch {
|
|
id = undefined
|
|
console.debug('Invalid existing ID token.')
|
|
}
|
|
}
|
|
if (id == null) {
|
|
// ask for a new one from google using the refresh token
|
|
try {
|
|
const resp = await requestFirebaseIdToken(refresh)
|
|
console.debug('Obtained fresh ID token from Firebase.')
|
|
id = resp.id_token
|
|
refresh = resp.refresh_token
|
|
} catch (e) {
|
|
// big unexpected problem -- functionally, they are not logged in
|
|
console.error(e)
|
|
return null
|
|
}
|
|
}
|
|
|
|
// step 2: given a valid ID token, ensure a valid custom token, and sign in
|
|
// to the client SDK with the custom token
|
|
if (custom != null) {
|
|
// sign in with this token, or throw it out if it's invalid/expired
|
|
try {
|
|
const creds = await signInWithCustomToken(clientAuth, custom)
|
|
console.debug('Signed in with custom token.')
|
|
return { creds, id, refresh, custom }
|
|
} catch {
|
|
custom = undefined
|
|
console.debug('Invalid existing custom token.')
|
|
}
|
|
}
|
|
if (custom == null) {
|
|
// ask for a new one from our cloud functions using the ID token, then sign in
|
|
try {
|
|
const resp = await requestManifoldCustomToken(id)
|
|
console.debug('Obtained fresh custom token from backend.')
|
|
custom = resp.token
|
|
const creds = await signInWithCustomToken(clientAuth, custom)
|
|
console.debug('Signed in with custom token.')
|
|
return { creds, id, refresh, custom }
|
|
} catch (e) {
|
|
// big unexpected problem -- functionally, they are not logged in
|
|
console.error(e)
|
|
return null
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
export const authenticateOnServer = async (ctx: RequestContext) => {
|
|
console.debug('Server authentication sequence starting.')
|
|
const tokens = await authAndRefreshTokens(ctx)
|
|
console.debug('Finished checking and refreshing tokens.')
|
|
const creds = tokens?.creds
|
|
try {
|
|
if (tokens == null) {
|
|
deleteTokenCookies(ctx.res)
|
|
console.debug('Not logged in; cleared token cookies.')
|
|
} else {
|
|
setTokenCookies(tokens, ctx.res)
|
|
console.debug('Logged in; set current token cookies.')
|
|
}
|
|
} catch (e) {
|
|
// definitely not supposed to happen, but let's be maximally robust
|
|
console.error(e)
|
|
}
|
|
return creds ?? null
|
|
}
|
|
|
|
// note that we might want to define these types more generically if we want better
|
|
// type safety on next.js stuff... see the definition of GetServerSideProps
|
|
|
|
type GetServerSidePropsAuthed<P> = (
|
|
context: GetServerSidePropsContext,
|
|
creds: UserCredential
|
|
) => Promise<GetServerSidePropsResult<P>>
|
|
|
|
export const redirectIfLoggedIn = <P>(
|
|
dest: string,
|
|
fn?: GetServerSideProps<P>
|
|
) => {
|
|
return async (ctx: GetServerSidePropsContext) => {
|
|
const creds = await authenticateOnServer(ctx)
|
|
if (creds == null) {
|
|
if (fn == null) {
|
|
return { props: {} }
|
|
} else {
|
|
const props = await fn(ctx)
|
|
console.debug('Finished getting initial props for rendering.')
|
|
return props
|
|
}
|
|
} else {
|
|
console.debug(`Redirecting to ${dest}.`)
|
|
return { redirect: { destination: dest, permanent: false } }
|
|
}
|
|
}
|
|
}
|
|
|
|
export const redirectIfLoggedOut = <P>(
|
|
dest: string,
|
|
fn?: GetServerSidePropsAuthed<P>
|
|
) => {
|
|
return async (ctx: GetServerSidePropsContext) => {
|
|
const creds = await authenticateOnServer(ctx)
|
|
if (creds == null) {
|
|
console.debug(`Redirecting to ${dest}.`)
|
|
return { redirect: { destination: dest, permanent: false } }
|
|
} else {
|
|
if (fn == null) {
|
|
return { props: {} }
|
|
} else {
|
|
const props = await fn(ctx, creds)
|
|
console.debug('Finished getting initial props for rendering.')
|
|
return props
|
|
}
|
|
}
|
|
}
|
|
}
|