Skip to content

Commit

Permalink
Add Firebase Auth Component (#48)
Browse files Browse the repository at this point in the history
initial firebase auth component
  • Loading branch information
abhinavkrin authored Jan 28, 2022
1 parent 8c2999d commit 65d5d88
Show file tree
Hide file tree
Showing 21 changed files with 10,317 additions and 5,448 deletions.
18 changes: 18 additions & 0 deletions app/.env.local.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Environment variables for firebase-admin
FIREBASE_PROJECT_ID=required
FIREBASE_PRIVATE_KEY=required
FIREBASE_CLIENT_EMAIL=required
FIREBASE_DATABASE_URL=if needed, see initAuth in /app/components/auth/firebase/lib/functions.js

# Environment variables for firebase client config.
NEXT_PUBLIC_FIREBASE_PROJECT_ID=required
NEXT_PUBLIC_FIREBASE_API_KEY=required
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=required
NEXT_PUBLIC_FIREBASE_APP_ID=required
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=

# for cookies signing
COOKIE_SECRET_CURRENT=
COOKIE_SECRET_PREVIOUS=
22 changes: 22 additions & 0 deletions app/components/auth/NoUserAvatar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FaUser } from "react-icons/fa";

export function NoUserAvatar({size,name}){
const char = name ? name.charAt(0) : null;
return (
<div
className="d-flex justify-content-center align-items-center"
style={{
width: `${size}px`,
height: `${size}px`,
background: "#dee2e6",
borderRadius: "50%"
}}>
{
char ?
<span style={{fontSize: `${size*0.65}px`}}>{char}</span>
:
<FaUser size={`${size*0.45}px`}/>
}
</div>
)
}
79 changes: 79 additions & 0 deletions app/components/auth/firebase/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
### Set up

1. set up environment variables for firebase config in `.env.local`

```
# Environment variables for firebase-admin.
# You can also find these values in the service-key.json file that you downloaded from firebase.
FIREBASE_PROJECT_ID=required
FIREBASE_PRIVATE_KEY=required
FIREBASE_CLIENT_EMAIL=required
FIREBASE_DATABASE_URL=if needed, see initAuth in /app/components/auth/firebase/lib/functions.js
# Environment variables for firebase client config.
NEXT_PUBLIC_FIREBASE_PROJECT_ID=required
NEXT_PUBLIC_FIREBASE_API_KEY=required
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=required
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=required
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=
# for cookies signing
COOKIE_SECRET_CURRENT=example_string_feafh3r2rv4ty4893vyt5vt5t38vy5n9t5vyt8vn54
COOKIE_SECRET_PREVIOUS=example_string_u4ht83r3m20rxc34nty340v9t4ty340mtu438ty48ntv4y8v
```
2. Initialize `next-firebase-auth` in _app.js
```
// ./pages/_app.js
import { initAuth } from '../components/auth/firebase';
initAuth()
..
..
```
3. Export the page component with `withAuthUser(Page)` and use `useAuthUser()` hook to get user info.
```
// ./pages/demo
import React from 'react'
import {
useAuthUser,
withAuthUser,
} from 'next-firebase-auth'
const Demo = () => {
const AuthUser = useAuthUser()
return (
<div>
<p>Your email is {AuthUser.email ? AuthUser.email : 'unknown'}.</p>
</div>
)
}
export default withAuthUser()(Demo)
```
4. For SSR, use `withAuthUserSSR` to wrap your getServerSideProps.

```
export const getServerSideProps = withAuthUserSSR(options)(({AuthUser}) => { })
```

For more details, see [https://github.com/gladly-team/next-firebase-auth](https://github.com/gladly-team/next-firebase-auth)

5. The `AuthUI` component handles UI for login and signup.

6. To use in development env. Set, `secure: false` in cookies config in `initAuth` function in file `/app/components/auth/firebase/lib/functions.js`

```
cookies: {
...
....
sameSite: 'strict',
secure: false, // set this to false in local (non-HTTPS) development
signed: true,
},
```
7. Build will fail if environment variables are not set. In order not to affect rendering of other components make use of `withFirebaseAuthUser`, `withFirebaseAuthUserSSR`, `withFirebaseAuthUserTokenSSR` and `useFirebaseAuthUser` instead of their respective `next-firebase-auth` version. They are just a wrapper that checks if firebase is initialised properly or not. And prevents build fail. They are exported from `/app/components/auth/firebase`.
20 changes: 20 additions & 0 deletions app/components/auth/firebase/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import functions from "./lib/functions";
import firebaseAuthMenuButtonModule from "./ui/FirebaseAuthMenuButton";
import firebaseAuthUIModule from "./ui/FirebaseAuthUI";
import firebaseLogimFormModule from "./ui/FirebaseLoginForm";
import firebaseSignupFormModule from "./ui/FirebaseSignupForm";
import firebaseUserInfoModule from "./ui/FirebaseUserInfo";

export const withFirebaseAuthUser = functions.withFirebaseAuthUser;
export const withFirebaseAuthUserSSR = functions.withFirebaseAuthUserSSR;
export const withFirebaseAuthUserTokenSSR = functions.withFirebaseAuthUserTokenSSR;
export const useFirebaseAuthUser = functions.useFirebaseAuthUser;
export const getInitAuthResult = functions.getInitAuthResult;
export const initAuth = functions.initAuth;
export const createEmptyAuthUser = functions.createEmptyAuthUser;

export const FirebaseAuthMenuButton = firebaseAuthMenuButtonModule;
export const FirebaseAuthUI = firebaseAuthUIModule;
export const FirebaseLoginForm = firebaseLogimFormModule;
export const FirebaseSignupForm = firebaseSignupFormModule;
export const FirebaseUserInfo = firebaseUserInfoModule;
1 change: 1 addition & 0 deletions app/components/auth/firebase/lib/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const FB_APP_NAME = '[DEFAULT]';
179 changes: 179 additions & 0 deletions app/components/auth/firebase/lib/functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { init, useAuthUser, withAuthUser, withAuthUserSSR, withAuthUserTokenSSR} from 'next-firebase-auth'

const initAuthHelper = (function(){
let initAuthResult = {success: false, error: new Error('Firebase auth is not yet initialised')};
const getInitAuthResult = () => {
return initAuthResult;
}
const setInitAuthResult = ({success,error}) => {
initAuthResult = {success,error};
}
return {getInitAuthResult, setInitAuthResult};
})();

export const getInitAuthResult = initAuthHelper.getInitAuthResult;

export const createEmptyAuthUser = () => {
return {
id: null,
email: null,
emailVerified: false,
phoneNumber: null,
displayName: null,
photoURL: null,
claims: {},
getIdToken: async () => null,
clientInitialized: false,
firebaseUser: null,
signOut: async () => null,
serialize: ({ includeToken = true } = {}) =>
JSON.stringify({
id: null,
claims: {},
email: null,
emailVerified: false,
phoneNumber: null,
displayName: null,
photoURL: null,
clientInitialized: false,
...(includeToken && { _token: null }),
}),
}
}

export const initAuth = () => {
try {
init({
loginAPIEndpoint: '/api/fb/login', // required
logoutAPIEndpoint: '/api/fb/logout', // required
onLoginRequestError: (err) => {
console.error(err)
},
onLogoutRequestError: (err) => {
console.error(err)
},
firebaseAdminInitConfig: {
credential: {
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
// The private key must not be accessible on the client side.
privateKey: process.env.FIREBASE_PRIVATE_KEY,
},
databaseURL: process.env.FIREBASE_DATABASE_URL,
},

firebaseClientInitConfig: {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, // required
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
},
cookies: {
name: 'RC4Community', // required
// Keys are required unless you set `signed` to `false`.
// The keys cannot be accessible on the client side.
keys: [
process.env.COOKIE_SECRET_CURRENT,
process.env.COOKIE_SECRET_PREVIOUS,
],
httpOnly: true,
maxAge: 12 * 60 * 60 * 24 * 1000, // twelve days
overwrite: true,
path: '/',
sameSite: 'strict',
secure: true, // set this to false in local (non-HTTPS) development
signed: true,
},
onVerifyTokenError: (err) => {
console.error(err)
},
onTokenRefreshError: (err) => {
console.error(err)
},
});
initAuthHelper.setInitAuthResult({error: null, success: true});
} catch (e) {
console.error(e)
initAuthHelper.setInitAuthResult({error: e, success: false});
}
return initAuthHelper.getInitAuthResult();
}

export const withFirebaseAuthUser = (options) => (ChildComponent) => {
const WithFirebaseAuthUserHOC = props => {
if(initAuthHelper.getInitAuthResult().success){
const Component = withAuthUser(options)(ChildComponent);
return <Component {...props}/>
} else {
console.error(initAuthHelper.getInitAuthResult().error);
console.error("You must configure firebase auth before using firebase auth. See https://github.com/RocketChat/RC4Community/blob/firebase-auth/app/components/auth/firebase/README.md");
return <ChildComponent {...props} initAuthResult={initAuthHelper.getInitAuthResult()}/>
}
}
WithFirebaseAuthUserHOC.displayName = "WithFirebaseAuthUserHOC";
return WithFirebaseAuthUserHOC;
}

export const useFirebaseAuthUser = () => {
if(initAuthHelper.getInitAuthResult().success){
return useAuthUser();
} else {
return createEmptyAuthUser();
}
}

const handleFBNotInitError = async (context,getServerSidePropsFunc) => {
console.error(initAuthHelper.getInitAuthResult().error);
console.error("You must configure firebase auth before using firebase auth. See https://github.com/RocketChat/RC4Community/blob/firebase-auth/app/components/auth/firebase/README.md");

const AuthUser = createEmptyAuthUser();
context.AuthUser = AuthUser;

const AuthUserSerialized = AuthUser.serialize();
let returnData = {props: {AuthUserSerialized}};

if(getServerSidePropsFunc) {
// a getServerSideProps function is passed
const composedProps = (await getServerSidePropsFunc(context)) || {};
if(composedProps){
if(composedProps.props){
returnData = { ...composedProps }
returnData.props.AuthUserSerialized = AuthUserSerialized
} else if(composedProps.notFound || composedProps.redirect) {
// If composedProps returned a 'notFound' or 'redirect' key
// (as per official doc: https://nextjs.org/docs/api-reference/data-fetching/get-server-side-props)
// it means it contains a custom dynamic routing logic that should not be overwritten
returnData = {...composedProps}
}
}
}
return returnData;
}

export const withFirebaseAuthUserSSR = (options) => (getServerSidePropsFunc) => async (context) => {
if(initAuthHelper.getInitAuthResult().success){
return withAuthUserSSR(options)(getServerSidePropsFunc)(context);
} else {
// firebase is uninitialised due to some error
return (await handleFBNotInitError(context,getServerSidePropsFunc));
}
}

export const withFirebaseAuthUserTokenSSR = (options) => (getServerSidePropsFunc) => async (context) => {
if(initAuthHelper.getInitAuthResult().success){
return withAuthUserTokenSSR(options)(getServerSidePropsFunc)(context);
} else {
// firebase is uninitialised due to some error
return (await handleFBNotInitError(context,getServerSidePropsFunc));
}
}

export default {
getInitAuthResult,
createEmptyAuthUser,
initAuth,
useFirebaseAuthUser,
withFirebaseAuthUser,
withFirebaseAuthUserSSR,
withFirebaseAuthUserTokenSSR
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.authDialogWrapper {
position: relative;
}
.authContainer {
display: block;
position: absolute;
right: 8px;
top: 62px;
width: 354px;
max-height: -webkit-calc(100vh - 62px - 100px);
max-height: calc(100vh - 62px - 100px);
overflow-y: auto;
overflow-x: hidden;
border-radius: 8px;
margin-left: 12px;
z-index: 991;
line-height: normal;
background: #fff;
border: 1px solid #ccc;
border-color: rgba(0,0,0,.2);
color: #000;
-webkit-box-shadow: 0 2px 10px rgb(0 0 0 / 20%);
box-shadow: 0 2px 10px rgb(0 0 0 / 20%);
-webkit-user-select: text;
user-select: text;
}

.avatar {
background: var(--bs-gray-300);
border-radius: 50%;
width: 42px;
height: 42px;
display: flex;
justify-content: center;
align-items: center;
}

.avatarButton {
background: none;
border: none;
}

.avatarButton:focus {
outline: none;
}
7 changes: 7 additions & 0 deletions app/components/auth/firebase/styles/FirebaseAuthUI.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.authUIWrapper {
width: 100%;
max-width: 400px;
border: 1px solid #ddd;
box-shadow: 2px 2px 3px 3px #0000001D;
background: #FFF;
}
Loading

0 comments on commit 65d5d88

Please sign in to comment.