Skip to content

Commit fa79bd8

Browse files
authored
Fix email verification links and publishing shortly after verification (#1049)
* Disable i18n logging by default * Redirect auth handler paths to existing firebase hosting * Refresh auth token on change Refresh the auth token when profile role or email verified changes. * Remove logs * Fix index * simplify/fix firebase auth redirect
1 parent 3863b09 commit fa79bd8

File tree

5 files changed

+87
-21
lines changed

5 files changed

+87
-21
lines changed

components/auth/service.tsx

+59-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { useProfile } from "components/db"
2+
import { useProfileState } from "components/db/profile/redux"
13
import { User } from "firebase/auth"
24
import Router, { useRouter } from "next/router"
3-
import React, { useEffect } from "react"
5+
import React, { useCallback, useEffect, useRef } from "react"
46
import { auth } from "../firebase"
57
import { useAppDispatch } from "../hooks"
68
import { createService } from "../service"
@@ -9,21 +11,64 @@ import { Claim } from "./types"
911

1012
export const { Provider } = createService(() => {
1113
const dispatch = useAppDispatch()
12-
useEffect(
13-
() =>
14-
auth.onAuthStateChanged(async user => {
15-
let claims: Claim | undefined = undefined
16-
if (user) {
17-
const token = await user.getIdTokenResult()
18-
const fromToken = Claim.validate(token.claims)
19-
if (fromToken.success) claims = fromToken.value
20-
}
21-
dispatch(authChanged({ user, claims }))
22-
}),
23-
[dispatch]
24-
)
14+
const getToken = useGetTokenWithRefresh()
15+
16+
useEffect(() => {
17+
const unsubscribe = auth.onAuthStateChanged(async user => {
18+
let claims: Claim | undefined = undefined
19+
if (user) {
20+
let token = await getToken(user)
21+
const fromToken = Claim.validate(token.claims)
22+
if (fromToken.success) claims = fromToken.value
23+
}
24+
dispatch(authChanged({ user, claims }))
25+
})
26+
return () => {
27+
unsubscribe()
28+
}
29+
}, [dispatch, getToken])
2530
})
2631

32+
/**
33+
* The token does not refresh when the user's profile or claims in firebase auth change,
34+
* So we need to manually refresh it if the user's profile or claims change.
35+
* It's always possible (but a bug) that the profile is out of sync with the auth database,
36+
* so we need to avoid refresh loops by only refreshing once per service instance.
37+
*/
38+
const useGetTokenWithRefresh = () => {
39+
const hasRefreshedToken = useRef(false)
40+
const profile = useProfileState(),
41+
hasProfile = !profile.loading,
42+
roleInProfile = profile.profile?.role
43+
44+
return useCallback(
45+
async (user: User) => {
46+
let token = await user.getIdTokenResult()
47+
48+
const isRoleOutOfSyncInToken =
49+
hasProfile && roleInProfile !== token.claims.role,
50+
isEmailVerifiedOutOfSyncInToken =
51+
user.emailVerified !== Boolean(token.claims.email_verified),
52+
isTokenOutOfSync =
53+
isRoleOutOfSyncInToken || isEmailVerifiedOutOfSyncInToken
54+
55+
// If the profile has a role but the token doesn't, try refreshing the token.
56+
// If the user indicates email is verified but the token doesn't, try refreshing the token.
57+
// Only refresh once per service instance.
58+
if (isTokenOutOfSync && !hasRefreshedToken.current) {
59+
console.log(
60+
"Refreshing token because it is out of sync with profile or user"
61+
)
62+
token = await user.getIdTokenResult(true)
63+
hasRefreshedToken.current = true
64+
}
65+
66+
return token
67+
},
68+
[hasProfile, roleInProfile]
69+
)
70+
}
71+
2772
/**
2873
* Renders the given component if authenticated, otherwise redirects.
2974
*/

firestore.indexes.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"queryScope": "COLLECTION_GROUP",
77
"fields": [
88
{
9-
"fieldPath": "authorUid",
9+
"fieldPath": "authorRole",
1010
"order": "ASCENDING"
1111
},
1212
{

next-i18next.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @type {import('next-i18next').UserConfig}
33
*/
44
module.exports = {
5-
debug: process.env.NODE_ENV === "development",
5+
debug: Boolean(process.env.I18N_DEBUG),
66
i18n: {
77
defaultLocale: "en",
88
locales: ["en"]

next.config.js

+24-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
/**
2-
* @type {import('next').NextConfig}
3-
*/
4-
51
const i18Config = require("./next-i18next.config")
62

73
const config = {
@@ -17,8 +13,12 @@ const config = {
1713
i18n: i18Config.i18n
1814
}
1915

16+
/** @type {import('next').NextConfig} */
2017
module.exports = {
2118
...config,
19+
async redirects() {
20+
return [redirectFirebaseAuthHandlers()]
21+
},
2222
async rewrites() {
2323
return [
2424
{
@@ -28,3 +28,23 @@ module.exports = {
2828
]
2929
}
3030
}
31+
32+
/**
33+
* Redirect auth handler paths to the hosted firebase pages. This avoids having
34+
* to implement these pages ourselves in next.js but continues to depend on
35+
* firebase hosting a bit.
36+
*b
37+
* In production, FIREBASE_AUTH_DOMAIN is set to digital-testimony-prod.web.app.
38+
*
39+
* @see https://firebase.google.com/docs/auth/custom-email-handler
40+
*/
41+
const redirectFirebaseAuthHandlers = () => {
42+
const firebaseDomain =
43+
process.env.FIREBASE_AUTH_DOMAIN ?? "digital-testimony-dev.web.app"
44+
45+
return {
46+
source: "/__/:path*",
47+
destination: `https://${firebaseDomain}/__/:path*`,
48+
permanent: false
49+
}
50+
}

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
6-
"build": "next build",
6+
"build:dev": "MAPLE_ENV=dev next build",
7+
"build:prod": "MAPLE_ENV=prod next build",
78
"build:functions": "next lint -d functions/src && yarn --cwd functions build",
89
"check-formatting": "prettier --check .",
910
"check-types": "tsc --noEmit",

0 commit comments

Comments
 (0)