Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Firebase Auth Component #48

Merged
merged 20 commits into from
Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`,
RonLek marked this conversation as resolved.
Show resolved Hide resolved
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