-
Notifications
You must be signed in to change notification settings - Fork 0
/
auth.js
299 lines (251 loc) · 8.38 KB
/
auth.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
import React, {
useState,
useEffect,
useMemo,
useContext,
createContext,
} from "react";
import queryString from "query-string";
import firebase from "./firebase";
import { useUser, createUser, updateUser } from "./db";
import { history } from "./router";
import PageLoader from "./../components/PageLoader";
import { getFriendlyPlanId } from "./prices";
// Whether to merge user data from database into auth.user
const MERGE_DB_USER = true;
const authContext = createContext();
// Context Provider component that wraps your app and makes auth object
// available to any child component that calls the useAuth() hook.
export function ProvideAuth({ children }) {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}
// Hook that enables any component to subscribe to auth state
export const useAuth = () => {
return useContext(authContext);
};
// Provider hook that creates auth object and handles state
function useProvideAuth() {
// Store auth user object
const [user, setUser] = useState(null);
// Format final user object and merge extra data from database
const finalUser = usePrepareUser(user);
// Handle response from authentication functions
const handleAuth = async (response) => {
const { user, additionalUserInfo } = response;
// Ensure Firebase is actually ready before we continue
await waitForFirebase();
// Create the user in the database if they are new
if (additionalUserInfo.isNewUser) {
await createUser(user.uid, { email: user.email });
}
// Update user in state
setUser(user);
return user;
};
const signup = (email, password) => {
return firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(handleAuth);
};
const signin = (email, password) => {
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then(handleAuth);
};
const signinWithProvider = (name) => {
// Get provider data by name ("password", "google", etc)
const providerData = allProviders.find((p) => p.name === name);
const provider = new providerData.providerMethod();
if (providerData.parameters) {
provider.setCustomParameters(providerData.parameters);
}
return firebase.auth().signInWithPopup(provider).then(handleAuth);
};
const signout = () => {
return firebase.auth().signOut();
};
const sendPasswordResetEmail = (email) => {
return firebase.auth().sendPasswordResetEmail(email);
};
const confirmPasswordReset = (password, code) => {
// Get code from query string object
const resetCode = code || getFromQueryString("oobCode");
return firebase.auth().confirmPasswordReset(resetCode, password);
};
const updateEmail = (email) => {
return firebase
.auth()
.currentUser.updateEmail(email)
.then(() => {
// Update user in state (since onAuthStateChanged doesn't get called)
setUser(firebase.auth().currentUser);
});
};
const updatePassword = (password) => {
return firebase.auth().currentUser.updatePassword(password);
};
// Update auth user and persist to database (including any custom values in data)
// Forms can call this function instead of multiple auth/db update functions
const updateProfile = async (data) => {
const { email, name, picture } = data;
// Update auth email
if (email) {
await firebase.auth().currentUser.updateEmail(email);
}
// Update auth profile fields
if (name || picture) {
let fields = {};
if (name) fields.displayName = name;
if (picture) fields.photoURL = picture;
await firebase.auth().currentUser.updateProfile(fields);
}
// Persist all data to the database
await updateUser(user.uid, data);
// Update user in state
setUser(firebase.auth().currentUser);
};
useEffect(() => {
// Subscribe to user on mount
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
setUser(user);
} else {
setUser(false);
}
});
// Unsubscribe on cleanup
return () => unsubscribe();
}, []);
return {
user: finalUser,
signup,
signin,
signinWithProvider,
signout,
sendPasswordResetEmail,
confirmPasswordReset,
updateEmail,
updatePassword,
updateProfile,
};
}
// A Higher Order Component for requiring authentication
export const requireAuth = (Component) => {
return (props) => {
// Get authenticated user
const auth = useAuth();
useEffect(() => {
// Redirect if not signed in
if (auth.user === false) {
history.replace("/auth/signin");
}
}, [auth]);
// Show loading indicator
// We're either loading (user is null) or we're about to redirect (user is false)
if (!auth.user) {
return <PageLoader />;
}
// Render component now that we have user
return <Component {...props} />;
};
};
// Format final user object and merge extra data from database
function usePrepareUser(user) {
// Fetch extra data from database (if enabled and auth user has been fetched)
const userDbQuery = useUser(MERGE_DB_USER && user && user.uid);
// Memoize so we only create a new object if user or userDbQuery changes
return useMemo(() => {
// Return if auth user is null (loading) or false (not authenticated)
if (!user) return user;
// Data we want to include from auth user object
let finalUser = {
uid: user.uid,
email: user.email,
name: user.displayName,
picture: user.photoURL,
};
// Include an array of user's auth providers, such as ["password", "google", etc]
// Components can read this to prompt user to re-auth with the correct provider
finalUser.providers = user.providerData.map(({ providerId }) => {
return allProviders.find((p) => p.id === providerId).name;
});
// If merging user data from database is enabled ...
if (MERGE_DB_USER) {
switch (userDbQuery.status) {
case "loading":
// Return null user so auth is considered loading until we have db data to merge
return null;
case "error":
// Log query error to console
console.error(userDbQuery.error);
return null;
case "success":
// If user data doesn't exist we assume this means user just signed up and the createUser
// function just hasn't completed. We return null to indicate a loading state.
if (userDbQuery.data === null) return null;
// Merge user data from database into finalUser object
Object.assign(finalUser, userDbQuery.data);
// Get values we need for setting up some custom fields below
const { stripePriceId, stripeSubscriptionStatus } = userDbQuery.data;
// Add planId field (such as "basic", "premium", etc) based on stripePriceId
if (stripePriceId) {
finalUser.planId = getFriendlyPlanId(stripePriceId);
}
// Add planIsActive field and set to true if subscription status is "active" or "trialing"
finalUser.planIsActive = ["active", "trialing"].includes(
stripeSubscriptionStatus
);
// no default
}
}
return finalUser;
}, [user, userDbQuery]);
}
const allProviders = [
{
id: "password",
name: "password",
},
{
id: "google.com",
name: "google",
providerMethod: firebase.auth.GoogleAuthProvider,
},
{
id: "facebook.com",
name: "facebook",
providerMethod: firebase.auth.FacebookAuthProvider,
parameters: {
// Tell fb to show popup size UI instead of full website
display: "popup",
},
},
{
id: "twitter.com",
name: "twitter",
providerMethod: firebase.auth.TwitterAuthProvider,
},
{
id: "github.com",
name: "github",
providerMethod: firebase.auth.GithubAuthProvider,
},
];
// Waits on Firebase user to be initialized before resolving promise
// This is used to ensure auth is ready before any writing to the db can happen
const waitForFirebase = () => {
return new Promise((resolve) => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
resolve(user); // Resolve promise when we have a user
unsubscribe(); // Prevent from firing again
}
});
});
};
const getFromQueryString = (key) => {
return queryString.parse(window.location.search)[key];
};