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 10 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
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>
)
}
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]';
62 changes: 62 additions & 0 deletions app/components/auth/firebase/lib/functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { init } from 'next-firebase-auth'
export const getFirebaseConfig = () => ({
abhinavkrin marked this conversation as resolved.
Show resolved Hide resolved
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID
});


export const initAuth = () => {
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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having the user to extract these fields from the service-account file can we instead give the path to the sa file itself? Ref: https://stackoverflow.com/a/66984271/8316412

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can. But while deploying users may refrain from uploading their service-key.json file. We can set the contents of the file in an environment variable. For now, I have edited the readme.md for firebase auth and mentioned where to find the values. Waiting for your comment on this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users wouldn't be required to upload their sa file to Git. Instead of scouring through the file for the variables (3 vars), it would be far easier to directly give path to the sa file (1 var), while having the user keep their file even outside the RC4Community project (the absolute path would be specified in this case).

Moreover, there is a case of having multiple sa files with different privileges. Instead of replacing the 3 env variables every time, it'll be easier for the user to just change the path to the file.

},
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)
},
})
}
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;
}
32 changes: 32 additions & 0 deletions app/components/auth/firebase/ui/FirebaseAuthMenuButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState } from "react";
import { useAuthUser } from "next-firebase-auth";
import { FirebaseAuthUI } from "./FirebaseAuthUI";
import { NoUserAvatar } from "../../NoUserAvatar";
import styles from "../styles/FirebaseAuthMenuButton.module.css";
export function FirebaseAuthMenuButton({}){
const user = useAuthUser();
const [isOpen,setOpen] = useState(false);
return (
<div className={styles.authDialogWrapper}>
<div className={styles.avatar}>
<button className={styles.avatarButton} onClick={() => setOpen(!isOpen)}>
<span className="d-flex align-items-center">
{
user?.photoURL ?
<img src={user.photoURL}
alt={user.displayName}
className="rounded-circle"
height="42px"
width="42px" />
:
<NoUserAvatar name={user?.displayName} size="42" />
}
</span>
</button>
</div>
{ isOpen &&
<div className={styles.authContainer}><FirebaseAuthUI/></div>
}
</div>
)
}
45 changes: 45 additions & 0 deletions app/components/auth/firebase/ui/FirebaseAuthUI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useAuthUser } from "next-firebase-auth";
import { FirebaseLoginForm } from "./FirebaseLoginForm";
import { useState } from "react";
import { Button } from "react-bootstrap";
import { FirebaseSignupForm } from "./FirebaseSignupForm";
import { FirebaseUserInfo } from "./FirebaseUserInfo";
import styles from '../styles/FirebaseAuthUI.module.css';

export function FirebaseAuthUI(){
const user = useAuthUser();
const [signupVisible,setSignupVisible] = useState(false);
if(user.id){
return (
<div className={styles.authUIWrapper}>
<FirebaseUserInfo/>
</div>
);
} else if(signupVisible){
return (
<div className={styles.authUIWrapper}>
<div className="w-100 p-1 d-flex align-items-center justify-content-center bg-light">
<Button
style={{position: "absolute", left: "5px"}}
variant="light"
size="sm"
onClick={()=>setSignupVisible(false)}>
&lt; back
</Button>
&nbsp;
<span>Sign up</span>
</div>
<FirebaseSignupForm onSignupComplete={()=>setSignupVisible(false)}/>
</div>
);
} else {
return (
<div className={styles.authUIWrapper}>
<div className="w-100 p-1 d-flex align-items-center justify-content-center bg-light">
<span>Log in</span>
</div>
<FirebaseLoginForm onSignupClick={()=>setSignupVisible(true)}/>
</div>
);
}
}
162 changes: 162 additions & 0 deletions app/components/auth/firebase/ui/FirebaseLoginForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import {
EmailAuthProvider,
FacebookAuthProvider,
fetchSignInMethodsForEmail,
getAuth,
GoogleAuthProvider,
linkWithCredential,
OAuthProvider,
signInWithEmailAndPassword,
signInWithPopup
} from "firebase/auth";
import {getApp} from 'firebase/app';
import { useState } from "react";
import { FormControl, Alert, Button } from "react-bootstrap";
import {FB_APP_NAME} from '../lib/constants';
export function FirebaseLoginForm({onSignupClick}){
const [email,setEmail] = useState("");
const [password,setPassword] = useState("");
const [errorMessage,setError] = useState("");
const [diffCredError,setDiffCredError] = useState(null);
const [progress,setProgress] = useState(false);

const doEmailPasswordLogin = async (e) => {
e.preventDefault();
if(progress){
return true;
}
setProgress(true);
try {
const fbApp = getApp(FB_APP_NAME);
const userCred = await signInWithEmailAndPassword(getAuth(fbApp),email,password);

if(diffCredError?.oldProvider?.providerId === EmailAuthProvider.PROVIDER_ID){
// The signin was requested to link new credentials with the account
await linkWithCredential(userCred.user,OAuthProvider.credentialFromError(diffCredError.error));
}

} catch(error){
switch(error.code){
case 'auth/user-not-found':
setError("User not found");
break;
case 'auth/wrong-password':
setError("Incorrect Password");
break;
default:
setError("Unknown error occurred");
}
} finally {
setProgress(false);
}
}
const handleProviderSignIn = async provider => {
if(progress){
return;
}
const fbApp = getApp(FB_APP_NAME);
const auth = getAuth(fbApp);
try {
const userCred = await signInWithPopup(auth,provider);
if(diffCredError){
// The signin was requested to link new credentials with the account
await linkWithCredential(userCred.user,OAuthProvider.credentialFromError(diffCredError.error));
}
} catch (e){
switch(e.code){
case 'auth/popup-closed-by-user':
case 'auth/cancelled-popup-request':
break;
case 'auth/popup-blocked':
setError("Popup blocked by your browser.")
break;
case 'auth/account-exists-with-different-credential':
const methods = await fetchSignInMethodsForEmail(auth,e.customData.email);;
setDiffCredError({error: e, newProviderId: provider.providerId ,oldProviderId: methods[0]});
break;
default:
setError("Unknown error occurred");
}
setProgress(false);
}
}

const onGoogleBtnClick = () => {
if(progress){
return;
}
setProgress(true);
const provider = new GoogleAuthProvider();
handleProviderSignIn(provider);
}

const onFacebookBtnClick = () => {
if(progress){
return;
}
setProgress(true);
const provider = new FacebookAuthProvider();
handleProviderSignIn(provider);
}

return (
<div className="container-fluid p-1">
<form className="container-fluid" onSubmit={doEmailPasswordLogin}>
<FormControl
type="text"
placeholder="email"
className="mb-1"
disabled={progress}
onChange={e=> setEmail(e.target.value)}/>
<FormControl
type="password"
placeholder="password"
className="mb-1"
disabled={progress}
onChange={e => setPassword(e.target.value)}/>
{
errorMessage &&
<Alert variant="danger" className="mb-1">{errorMessage}</Alert>
}
<div className="d-flex justify-content-between">
<Button
type="submit"
className="mb-1"
disabled={progress}>
Login
</Button>
<Button
className="mb-1"
variant="light"
disabled={progress}
onClick={onSignupClick}>
Sign up
</Button>
</div>
</form>
<div className="container-fluid d-flex flex-column">
<Button
variant="danger"
className="mb-1"
onClick={onGoogleBtnClick}
disabled={progress}>
Sign in with Google
</Button>
<Button
className="mb-1"
onClick={onFacebookBtnClick}
disabled={progress}>
Sign in with Facebook
</Button>
</div>
{
diffCredError &&
<div className="p-1 mb-1">
<Alert variant="danger" className="mb-1">
User's email already exists. Sign in with {diffCredError.oldProviderId} to link your {diffCredError.newProviderId} account.
</Alert>
</div>
}
</div>
)
}
Loading