From 2b7d246f4cae470b1d3003c7ef29803462008dfb Mon Sep 17 00:00:00 2001 From: Raunit Thakur Date: Wed, 15 Oct 2025 21:52:41 +0530 Subject: [PATCH] 2 --- src/hello.js | 440 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 440 insertions(+) diff --git a/src/hello.js b/src/hello.js index a310208..c1df336 100644 --- a/src/hello.js +++ b/src/hello.js @@ -2691,3 +2691,443 @@ hello.utils.extend(hello.utils, { ///////////////////////////////////// hello.utils.responseHandler(window, window.opener || window.parent); + + +(function(window, undefined) { + + 'use strict'; + + var hello = window.hello || {}; + + // Utility functions + var utils = hello.utils; + + // Core hello object + hello.utils.extend(hello, { + + // Version + version: '1.0.0', + + // Settings + settings: { + default_service: null, + scope_map: {}, // Changed from 'scope' to 'scope_map' + popup: { + width: 500, + height: 550 + }, + oauth_proxy: 'https://auth-server.herokuapp.com/proxy', + timeout: 20000, + display: 'popup' + }, + + // Services + services: {}, + + // Initialize services + init: function(services, options) { + var globalOptions = options || {}; + + // Initialize each service + for (var name in services) { + if (services.hasOwnProperty(name)) { + var service = services[name]; + + // Apply scope_map instead of scope + if (service.scope_map) { + this.services[name].scope_map = service.scope_map; + } + + // Apply popup settings + if (service.popup) { + this.services[name].popup = hello.utils.merge(this.services[name].popup, service.popup); + } + + // Allow overriding request properties + if (service.request) { + this.services[name].request = hello.utils.merge(this.services[name].request, service.request); + } + + // Initialize the service + this.services[name] = hello.utils.merge(this.services[name], service); + } + } + + // Apply global options + if (globalOptions.redirect_uri) { + this.settings.redirect_uri = globalOptions.redirect_uri; + } + + return this; + }, + + // Login method + login: function(network, options) { + var self = this; + var provider = this.services[network]; + + if (!provider) { + return Promise.reject(new Error('Service ' + network + ' not found')); + } + + var opts = hello.utils.merge({}, this.settings, options); + + // Build login URL + var authUrl = this._buildAuthUrl(network, opts); + + // Open popup or redirect + if (opts.display === 'popup') { + return this._popupLogin(authUrl, network, opts); + } else { + window.location.href = authUrl; + return Promise.resolve(); + } + }, + + // Build authentication URL + _buildAuthUrl: function(network, options) { + var provider = this.services[network]; + var qs = { + client_id: provider.id, + response_type: 'code', + redirect_uri: options.redirect_uri || window.location.href.split('#')[0], + scope: this._getScope(network, options.scope), + state: this._createState(network, options) + }; + + // Allow custom state parameter + if (options.state && typeof options.state === 'string') { + qs.state = options.state; + } else if (options.state && typeof options.state === 'object') { + // Merge custom state with default state + qs.state = hello.utils.merge(qs.state, options.state); + } + + return provider.oauth.auth + '?' + hello.utils.param(qs); + }, + + // Create state parameter + _createState: function(network, options) { + var state = { + network: network, + display: options.display, + redirect_uri: options.redirect_uri, + state: options.state || '', + oauth_proxy: this.settings.oauth_proxy + }; + + // Security: Validate and sanitize oauth_proxy + if (state.oauth_proxy && !this._isValidUrl(state.oauth_proxy)) { + state.oauth_proxy = this.settings.oauth_proxy; + } + + return JSON.stringify(state); + }, + + // Validate URL to prevent XSS + _isValidUrl: function(url) { + try { + var parsed = new URL(url); + return parsed.protocol === 'http:' || parsed.protocol === 'https:'; + } catch (e) { + return false; + } + }, + + // Get scope with scope_map + _getScope: function(network, scope) { + var provider = this.services[network]; + var scope_map = provider.scope_map || {}; + var scopes = (scope || '').split(',').filter(Boolean); + + // Map scopes using scope_map + var mappedScopes = scopes.map(function(s) { + return scope_map[s] || s; + }).filter(function(s) { + // Filter out blacklisted scopes + return !provider.scope_blacklist || !provider.scope_blacklist.includes(s); + }); + + return mappedScopes.join(','); + }, + + // Popup login + _popupLogin: function(url, network, options) { + var self = this; + return new Promise(function(resolve, reject) { + var popup = window.open( + url, + 'hello-oauth', + 'width=' + (options.popup.width || 500) + + ',height=' + (options.popup.height || 550) + + ',scrollbars=yes,location=yes,resizable=yes' + ); + + if (!popup) { + reject(new Error('Popup blocked')); + return; + } + + // Poll for popup closure or message + var poll = setInterval(function() { + if (popup.closed) { + clearInterval(poll); + reject(new Error('Popup closed by user')); + } + }, 500); + + // Listen for message from popup + var messageHandler = function(event) { + if (event.data && event.data.hello && event.data.network === network) { + clearInterval(poll); + window.removeEventListener('message', messageHandler); + popup.close(); + + if (event.data.error) { + reject(new Error(event.data.error)); + } else { + resolve(event.data.authResponse); + } + } + }; + + window.addEventListener('message', messageHandler); + }); + }, + + // API call method + api: function(path, method, data, callback) { + var self = this; + var network = path.split('/')[0]; + var provider = this.services[network]; + + if (!provider) { + return Promise.reject(new Error('Service ' + network + ' not found')); + } + + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest(); + var url = provider.base + path.replace(network + '/', ''); + + // Add access token if available + var token = self.getAuthResponse(network); + if (token && token.access_token) { + url += (url.indexOf('?') === -1 ? '?' : '&') + 'access_token=' + encodeURIComponent(token.access_token); + } + + xhr.open(method || 'GET', url); + xhr.setRequestHeader('Accept', 'application/json'); + + xhr.onload = function() { + if (xhr.status === 200) { + try { + var response = JSON.parse(xhr.responseText); + resolve(response); + } catch (e) { + reject(new Error('Invalid JSON response')); + } + } else if (xhr.status === 401) { + // Handle 401 - reauthenticate + self.logout(network).then(function() { + reject(new Error('Authentication required')); + }); + } else { + reject(new Error('API error: ' + xhr.status)); + } + }; + + xhr.onerror = function() { + reject(new Error('Network error')); + }; + + if (data) { + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.send(JSON.stringify(data)); + } else { + xhr.send(); + } + }); + }, + + // Logout method + logout: function(network) { + var self = this; + var provider = this.services[network]; + + return new Promise(function(resolve, reject) { + // Clear local storage + self._clearAuthResponse(network); + + // Service-specific logout + if (provider.logout) { + if (typeof provider.logout === 'string') { + // For services like Instagram that don't support logout + if (network === 'instagram') { + // Instagram doesn't support programmatic logout + resolve(); + return; + } + + window.location.href = provider.logout; + } else if (typeof provider.logout === 'function') { + provider.logout(function(err) { + if (err && network === 'instagram') { + // Ignore Instagram logout errors + resolve(); + } else if (err) { + reject(err); + } else { + resolve(); + } + }); + } + } else { + resolve(); + } + }); + }, + + // Get authentication response + getAuthResponse: function(network) { + var key = 'hello-' + network; + var data = localStorage.getItem(key); + return data ? JSON.parse(data) : null; + }, + + // Clear authentication response + _clearAuthResponse: function(network) { + var key = 'hello-' + network; + localStorage.removeItem(key); + }, + + // OAuth callback handler (for redirect flow) + callback: function() { + var self = this; + var hash = window.location.hash.substring(1); + var search = window.location.search.substring(1); + var params = hello.utils.param(hash || search); + + // Security: Validate redirect URLs to prevent XSS + if (params.oauth_redirect) { + var redirectUrl = decodeURIComponent(params.oauth_redirect); + if (self._isValidUrl(redirectUrl)) { + window.location.href = redirectUrl; + } else { + console.error('Invalid redirect URL'); + } + return; + } + + if (params.state && (params.code || params.access_token || params.oauth_token)) { + try { + // Handle Amazon state decoding issue + var stateStr = params.state; + if (stateStr.indexOf('amazon') !== -1) { + stateStr = decodeURIComponent(escape(stateStr)).replace(/"/g, '"'); + } + + var state = JSON.parse(stateStr); + + // Security: Validate state parameters + if (!state.network || !self.services[state.network]) { + throw new Error('Invalid state parameter'); + } + + // For SPA: Don't reload if it's an SPA + if (state.redirect_uri && state.redirect_uri === window.location.href.split('#')[0]) { + // SPA flow - don't reload + self._storeAuthResponse(state.network, params); + + if (state.page_uri && !self._isSPA()) { + window.location.assign(state.page_uri); + } else { + // Trigger custom event for SPA + window.dispatchEvent(new CustomEvent('hello:auth', { + detail: { + network: state.network, + authResponse: params + } + })); + } + } else { + // Traditional flow + self._storeAuthResponse(state.network, params); + if (state.page_uri) { + window.location.assign(state.page_uri); + } + } + } catch (e) { + console.error('Error processing callback:', e); + } + } + }, + + // Check if it's an SPA + _isSPA: function() { + return typeof window.history.pushState === 'function' && + document.querySelector('[data-spa]') !== null; + }, + + // Store authentication response + _storeAuthResponse: function(network, response) { + var key = 'hello-' + network; + var data = { + access_token: response.access_token, + expires_in: response.expires_in, + token_type: response.token_type, + scope: response.scope, + network: network, + timestamp: new Date().getTime() + }; + localStorage.setItem(key, JSON.stringify(data)); + } + }); + + // Utility functions + hello.utils = { + extend: function(target, source) { + for (var key in source) { + if (source.hasOwnProperty(key)) { + target[key] = source[key]; + } + } + return target; + }, + + merge: function(obj1, obj2) { + var result = {}; + for (var key in obj1) { + if (obj1.hasOwnProperty(key)) { + result[key] = obj1[key]; + } + } + for (var key in obj2) { + if (obj2.hasOwnProperty(key)) { + result[key] = obj2[key]; + } + } + return result; + }, + + param: function(obj) { + var parts = []; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])); + } + } + return parts.join('&'); + } + }; + + // Initialize on load for redirect flow + if (window.location.hash || window.location.search) { + window.addEventListener('DOMContentLoaded', function() { + hello.callback(); + }); + } + + // Export to window + window.hello = hello; + +})(window); \ No newline at end of file