import { Injectable, Inject } from '@angular/core'; import { AuthMethod } from './auth-method'; import { Observable, Subject } from 'rxjs'; import * as firebase from 'firebase'; /** * Vk firebase auth service * `vk.com` is not among standard auth providers in firebase, so we provide our own solution for * this. To get acquainted with the flow, please, read [this](https://vk.com/dev/implicit_flow_user) article. * * In order to avoid creation of http server, vkAuthService works only with firebase and not with REST API. * So, we strongly recommend to set redirect url to be a current location, thus one can get a token on a client side. * * Internal work of a service one can found in source tab. * * **Usage:** * * `VkConfigService` declaration * ```typescript * //vkAuthConfig.service.ts * import {Injectable} from "@angular/core"; * import {VkAuthConfig, VkConfig, PopupConfig} from "@nodeart/firebase-connector"; * * \@Injectable() * export class VkConfiguration implements VkAuthConfig { * public vkConfig : VkConfig = { * client_id: 'app_id', * display: 'popup', * scope: ['friends'], * response_type: 'token', * v: 5.63 * }; * public popupConfig: PopupConfig = { * location: 'no', * height: 600, * width: 600 * }; * public cleanUp: boolean = true; * public dbPath: string = 'auth/vk'; * constructor() { } * } * ``` * * `VkConfigService` register * * ```typescript * //someModule.module.ts * import {NgModule} from "@angular/core"; * import {VkConfiguration} from "./vkAuthConfig.service"; * \@NgModule({ * providers: [ * {provide: 'VkAuthConfig', useClass: VkConfiguration} * ] * }) * export class SomeModule { } * ``` * * Server side or [firebase cloud functions](https://firebase.google.com/docs/functions/) code: * ```javascript * 'use strict'; * * const admin = require('firebase-admin'); * const authWithVk = admin.database().ref('auth/vk'); * * const listener = (ref, snapshot) => { * const key = snapshot.key, * val = snapshot.val(); * * if (!val.processed) { * admin.auth() * .createCustomToken(val['access_token']) * .then(token => { * const data = Object.assign(val, { * access_token: token, * expires_in: null, * processed: true * }); * ref.child(key).set(data); * return data * }) * .then(data => console.log(`custom token generated = ${JSON.stringify(data)}`)) * } * }; * * authWithVk.on('child_added', snapshot => listener(authWithVk, snapshot)); * ``` * */ @Injectable() export class VkAuth implements AuthMethod { /** * Name of auth method */ name: string = 'Vk'; /** * VkAuth page base url; */ private authorize: string = 'https://oauth.vk.com/authorize'; constructor( @Inject('VkAuthConfig') private config : VkAuthConfig ) { this.config.vkConfig.redirect_uri = this.config.vkConfig.redirect_uri || window.location.href } /** * Login method */ login(): Promise { /** * Create a popup window and get its reference. * @type {Window} */ const vkAuthWindow = window.open( this.createUrl(this.config.vkConfig, this.authorize), '_blank', Object.keys(this.config.popupConfig).reduce( (acc, key) => acc += `${key}=${this.config.popupConfig[key]},`, '' ) ); return new Promise((resolve, reject) => { /** * flow: ---window.origin: blank----window.origin Error(cross-origin)----window.orogin OK */ let dbRef; const stopper = new Subject(); /** * each 1 second trigger an interval */ Observable.interval(1000) /** * that is mapped to location */ .map(() => vkAuthWindow.location) /** * path execution forward only if there is a hash in location (will block execution of unloaded window) */ .filter(location => !!location.hash) /** * path forward only if there is an access_token in hash, * otherwise throw an Error */ .map(location => { if (location.hash.indexOf('access_token') !== -1) { return location.hash; } else { throw new Error('no_token') } }) /** * stop interval execution after subject will trigger next function */ .takeUntil(stopper) /** * handle all errors that happen while execution and start observable again on failure * (will block cross-origin) errors */ .catch((err, outputObs) => outputObs) /** * transform hash to object */ .map(hash => hash.replace('#', '') .split('&') .reduce((acc, elem) => { const [key, val] = elem.split('='); acc[decodeURIComponent(key)] = decodeURIComponent(val); return acc; }, {}) ) /** * once we have reached subscribe, trigger next on subject and block interval execution */ .subscribe( auth => stopper.next(auth), err => console.log(err, 'err') ); /** * set object with access token and uid to firebase database */ stopper.flatMap((obj: Object) => { const db = firebase['database'](); dbRef = db.ref(this.config.dbPath).push(); dbRef.set(obj); return Observable.fromPromise(dbRef.once('child_changed') as Promise); }) /** * once the data is changed, get an access token */ .map(snapshot => snapshot.val()) /** * clean up */ .do(() => { if (this.config.cleanUp) { dbRef.remove(); } }) /** * get only not null values */ .filter(val => !!val) /** * take the value only once */ .first() /** * get only access token field */ .map(data => typeof data === 'object' ? data['token'] : data) /** * auth with custom token */ .flatMap(token => Observable.fromPromise(firebase['auth']().signInWithCustomToken(token) as Promise)) /** * close popup window */ .do(() => vkAuthWindow.close()) .subscribe( auth => resolve(auth), err => reject(err) ) }); } /** * utility function that converts an object to url * @returns {string} */ public createUrl(obj : Object, starter?: string): string { const join = (arr: Array): string => arr.join(','); return Object.keys(obj) .reduce( (acc: string, key: string, i: number): string => acc += `${i === 0 ? '?' : '&'}${key}=${ Array.isArray(obj[key]) ? join(obj[key]) : obj[key] }`, starter || '' ) } } /** * Utility interfaces */ export interface VkAuthConfig { popupConfig: PopupConfig; vkConfig: VkConfig; dbPath: string; cleanUp: boolean; } export interface VkConfig { client_id: string; redirect_uri?: string; scope: Array; display: string; response_type: string; v: string | number; state?: string; revoke?: number; } export interface PopupConfig { location: string; [key: string] : any; }