Skip to content

Instantly share code, notes, and snippets.

@steida
Created July 6, 2015 23:14
Show Gist options
  • Save steida/4838c8db9fc43d538bf5 to your computer and use it in GitHub Desktop.
Save steida/4838c8db9fc43d538bf5 to your computer and use it in GitHub Desktop.
Firebase helpers
import Firebase from 'firebase';
import Promise from 'bluebird';
import {ValidationError} from './lib/validation';
import {firebaseCursor} from './state';
// if (!process.env.IS_BROWSER) {
// // TODO: Set Firebase for server.
// }
export const TIMESTAMP = Firebase.ServerValue.TIMESTAMP;
export const firebase = new Firebase(firebaseCursor().get('url'));
// Explicitly passed loggedIn and isLoggedIn to prevent circular dependencies.
export function init(loggedIn, isLoggedIn) {
// Sync getAuth for asap rendering.
const authData = firebase.getAuth();
if (authData)
loggedIn(authData);
firebase.onAuth((authData) => {
// Just loggedIn.
if (authData && !isLoggedIn())
loggedIn(authData);
else if (!authData && isLoggedIn())
location.href = '/';
});
}
// Promisify Firebase onComplete callback.
export function promisify(callback: Function) {
return new Promise((resolve, reject) => {
// On failure, the first argument will be an Error object indicating the
// failure, with a machine-readable code attribute. On success, the first
// argument will be null and the second can be an object containing result.
callback((error, data) => {
if (error) {
reject(error);
return;
}
resolve(data);
});
});
}
function change(method: string, path: Array<string>, data) {
const child = firebase.child(path.join('/'));
return promisify(onComplete => {
const args = Array.prototype.slice.call(arguments, 2).concat(onComplete);
child[method].apply(child, args);
});
}
export function set(path: Array<string>, data): Promise {
return change('set', path, data);
}
export function push(path: Array<string>, data): Promise {
return change('push', path, data);
}
export function update(path: Array<string>, data): Promise {
return change('update', path, data);
}
export function remove(path: Array<string>): Promise {
return change('remove', path);
}
export function hasFirebaseErrorCode(error) {
const codes = [
'AUTHENTICATION_DISABLED',
'EMAIL_TAKEN',
'INVALID_ARGUMENTS',
'INVALID_CONFIGURATION',
'INVALID_CREDENTIALS',
'INVALID_EMAIL',
'INVALID_ORIGIN',
'INVALID_PASSWORD',
'INVALID_PROVIDER',
'INVALID_TOKEN',
'INVALID_USER',
'NETWORK_ERROR',
'PROVIDER_ERROR',
'TRANSPORT_UNAVAILABLE',
'UNKNOWN_ERROR',
'USER_CANCELLED',
'USER_DENIED'
];
return codes.indexOf(error.code) !== -1;
}
export function firebaseValidationError(error) {
if (!hasFirebaseErrorCode(error))
return error;
// Heuristic field detection.
const prop = error.message.indexOf('password') > -1 ? 'password' : 'email';
// Transform Firebase error to ValidationError.
return new ValidationError(error.message, prop);
}
export function onFirebaseError(error) {
throw error;
}
export function ensureId(value, id: ?string) {
// It's ok to override Firebase data.
if (id) {
if (value) value.id = id;
return value;
}
for (let id in value) value[id].id = id;
return value;
}
// Firebase key can't contain URL specific chars.
// http://stackoverflow.com/questions/24412393/how-to-index-by-email-in-firebase
// http://jsfiddle.net/katowulf/HLUc5/
// https://groups.google.com/forum/#!topic/firebase-talk/vtX8lfxxShk
export function escapeEmail(email) {
return email.replace(/\./g, ',');
}
export function unescapeEmail(email) {
return email.replace(/\,/g, '.');
}
export function encodeKey(uri) {
return encodeURIComponent(uri).replace(/\./g, '%2E');
}
export function decodeKey(uri) {
return decodeURIComponent(uri);
}
export function once(eventType, path: Array<string>) {
return new Promise((resolve, reject) => {
firebase
.child(path.join('/'))
.once(eventType, resolve, reject);
});
}
@steida
Copy link
Author

steida commented Jul 6, 2015

// Firebase usage in auth actions.
import User from '../users/user';
import setToString from '../lib/settostring';
import {dispatch} from '../dispatcher';
import {firebase, promisify, firebaseValidationError, set} from '../firebase';
import {validate} from '../validation';

export function updateFormField({target: {name, value}}) {
  // Both email and password max length is 256.
  value = value.slice(0, 256);
  dispatch(updateFormField, {name, value});
}

export function login(provider, params) {
  // Because Firebase is control freak requiring plain JS object.
  if (params) params = params.toJS();
  return dispatch(login, providerLogin(provider, params)
    .then(saveUser)
    .catch(error => {
      error = firebaseValidationError(error);
      authError(error);
      throw error;
    })
  );
}

export function authError(error) {
  dispatch(authError, error);
}

export function loggedIn(authData) {
  dispatch(loggedIn, authData);
}

export function logout() {
  firebase.unauth();
}

export function toggleForgetPasswordShown() {
  dispatch(toggleForgetPasswordShown);
}

export function resetPassword(params) {
  return dispatch(resetPassword, validate(params)
    .prop('email').required().email()
    .promise
      .then(() => {
        return promisify(onComplete => {
          firebase.resetPassword(params, onComplete);
        });
      })
      .catch(error => {
        error = firebaseValidationError(error);
        authError(error);
        throw error;
      }));
}

export function signup(params) {
  if (params) params = params.toJS();
  return dispatch(signup, validateForm(params)
    .then(() => {
      return promisify(onComplete => {
        firebase.createUser(params, onComplete);
      });
    })
    .then(() => authWithPassword(params))
    .then(saveUser)
    .catch(error => {
      error = firebaseValidationError(error);
      authError(error);
      throw error;
    }));
}

function providerLogin(provider, params) {
  return provider === 'email'
    ? emailLogin(params)
    : socialLogin(provider);
}

function emailLogin(params) {
  return validateForm(params)
    .then(() => authWithPassword(params));
}

function validateForm(params) {
  return validate(params)
    .prop('email').required().email()
    .prop('password').required().simplePassword()
    .promise;
}

function authWithPassword(params) {
  return promisify(onComplete => {
    firebase.authWithPassword(params, onComplete);
  });
}

// Never merge social accounts. Never treat email as key.
// http://grokbase.com/p/gg/firebase-talk/14a91gqjse/firebase-handling-multiple-providers
function socialLogin(provider) {
  return promisify(onComplete => {
    const options = {
      scope: {
        facebook: 'email,user_friends',
        google: 'email',
        twitter: ''
      }[provider]
    };
    // https://www.firebase.com/docs/web/guide/user-auth.html#section-popups
    firebase.authWithOAuthPopup(provider, (error, authData) => {
      if (error && error.code === 'TRANSPORT_UNAVAILABLE') {
        firebase.authWithOAuthRedirect(provider, onComplete, options);
        return;
      }
      onComplete(error, authData);
    }, options);
  });
}

function saveUser(auth) {
  const user = User.fromAuth(auth);
  return set(['users', user.id], user.toJS());
}

setToString('auth', {
  authError,
  loggedIn,
  login,
  logout,
  resetPassword,
  signup,
  toggleForgetPasswordShown,
  updateFormField
});

@ilionic
Copy link

ilionic commented Jul 15, 2015

Hey , couple of questions:

  1. How does fromAuth looks like inside of saveUser?
  2. How would you approach server-side auth that would work in conjunction with requireAuth component? So if user hits restricted route there is auth check before that to avoid redirects. Where is the best place to define isLoggedIn ?

@steida
Copy link
Author

steida commented Jul 22, 2015

@ilionic
Copy link

ilionic commented Aug 12, 2015

Wow, Finally! Thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment