130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
import { IncomingMessage, ServerResponse } from 'http'
|
|
import { Auth as FirebaseAuth, User as FirebaseUser } from 'firebase/auth'
|
|
import { AUTH_COOKIE_NAME } from 'common/envs/constants'
|
|
import { getCookies } from 'web/lib/util/cookie'
|
|
import {
|
|
GetServerSideProps,
|
|
GetServerSidePropsContext,
|
|
GetServerSidePropsResult,
|
|
} from 'next'
|
|
|
|
// client firebase SDK
|
|
import { app as clientApp } from './init'
|
|
import { getAuth, updateCurrentUser } from 'firebase/auth'
|
|
|
|
type RequestContext = {
|
|
req: IncomingMessage
|
|
res: ServerResponse
|
|
}
|
|
|
|
// The Firebase SDK doesn't really support persisting the logged-in state between
|
|
// devices, or anything like that. To get it from the client to the server:
|
|
//
|
|
// 1. We pack up the user by calling (the undocumented) User.toJSON(). This is the
|
|
// same way the Firebase SDK saves it to disk, so it's gonna have the right stuff.
|
|
//
|
|
// 2. We put it into a cookie and read the cookie out here.
|
|
//
|
|
// 3. We use the Firebase "persistence manager" to write the cookie value into the persistent
|
|
// store on the server (an in-memory store), just as if the SDK had saved the user itself.
|
|
//
|
|
// 4. We ask the persistence manager for the current user, which reads what we just wrote,
|
|
// and creates a real puffed-up internal user object from the serialized user.
|
|
//
|
|
// 5. We set that user to be the current Firebase user in the SDK.
|
|
//
|
|
// 6. We ask for the ID token, which will refresh it if necessary (i.e. if this cookie
|
|
// is from an old browser session), so that we know the SDK is prepared to do real
|
|
// Firebase queries.
|
|
//
|
|
// This strategy should be robust, since it's repurposing Firebase's internal persistence
|
|
// machinery, but the details may eventually need updating for new versions of the SDK.
|
|
//
|
|
// References:
|
|
// Persistence manager: https://github.com/firebase/firebase-js-sdk/blob/39f4635ebc07316661324145f1b8c27f9bd7aedb/packages/auth/src/core/persistence/persistence_user_manager.ts#L64
|
|
// Token manager: https://github.com/firebase/firebase-js-sdk/blob/39f4635ebc07316661324145f1b8c27f9bd7aedb/packages/auth/src/core/user/token_manager.ts#L76
|
|
|
|
interface FirebaseAuthInternal extends FirebaseAuth {
|
|
persistenceManager: {
|
|
fullUserKey: string
|
|
getCurrentUser: () => Promise<FirebaseUser | null>
|
|
persistence: {
|
|
_set: (k: string, obj: Record<string, unknown>) => Promise<void>
|
|
}
|
|
}
|
|
}
|
|
|
|
export const authenticateOnServer = async (ctx: RequestContext) => {
|
|
const user = getCookies(ctx.req.headers.cookie ?? '')[AUTH_COOKIE_NAME]
|
|
if (user == null) {
|
|
console.debug('User is unauthenticated.')
|
|
return null
|
|
}
|
|
try {
|
|
const deserializedUser = JSON.parse(user)
|
|
const clientAuth = getAuth(clientApp) as FirebaseAuthInternal
|
|
const persistenceManager = clientAuth.persistenceManager
|
|
const persistence = persistenceManager.persistence
|
|
await persistence._set(persistenceManager.fullUserKey, deserializedUser)
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const fbUser = (await persistenceManager.getCurrentUser())!
|
|
await fbUser.getIdToken() // forces a refresh if necessary
|
|
await updateCurrentUser(clientAuth, fbUser)
|
|
console.debug('Signed in with user from cookie.')
|
|
return fbUser
|
|
} catch (e) {
|
|
console.error(e)
|
|
return 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: FirebaseUser
|
|
) => Promise<GetServerSidePropsResult<P>>
|
|
|
|
export const redirectIfLoggedIn = <P extends { [k: string]: any }>(
|
|
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 extends { [k: string]: any }>(
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|