From 39faece84e71ed657878c1178d7f1dfdd28c14db Mon Sep 17 00:00:00 2001 From: Dale Hille Date: Tue, 16 Jun 2020 17:48:11 -0400 Subject: [PATCH] Fix: updates to accounts-ghe (fixes #201 , #200) (#317) * Fix: updates to accounts-ghe (fixes #201 , #200 ) - Make accounts-ghe a local package - don't send auth token to github as part of params - don't require GITHUB_API and GITHUB_URL for ghe logins - sanitize GHE url input on the initial razeedash configuration screen * add packages/accounts-ghe * Update packages * modernize accounts-ghe and make it a local package --- .meteor/packages | 2 +- .meteor/versions | 6 +- README.md | 14 --- imports/api/lib/ghe.js | 13 ++- imports/api/lib/login.js | 15 ++- imports/startup/client/index.js | 20 ++-- imports/startup/server/index.js | 4 +- imports/ui/pages/razeeWelcome/index.js | 3 + imports/ui/pages/razeeWelcome/page.html | 2 +- kubernetes/razeedash/resource.yaml | 12 --- package-lock.json | 18 ++-- packages/ghe/ghe.js | 21 +++++ packages/ghe/ghe_client.js | 57 ++++++++++++ packages/ghe/ghe_configure.html | 16 ++++ packages/ghe/ghe_configure.js | 13 +++ packages/ghe/ghe_login_button.css | 3 + packages/ghe/ghe_server.js | 118 ++++++++++++++++++++++++ packages/ghe/package.js | 30 ++++++ 18 files changed, 306 insertions(+), 61 deletions(-) create mode 100755 packages/ghe/ghe.js create mode 100755 packages/ghe/ghe_client.js create mode 100755 packages/ghe/ghe_configure.html create mode 100755 packages/ghe/ghe_configure.js create mode 100755 packages/ghe/ghe_login_button.css create mode 100755 packages/ghe/ghe_server.js create mode 100755 packages/ghe/package.js diff --git a/.meteor/packages b/.meteor/packages index 0b1a308c..a18085ba 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -46,7 +46,7 @@ meteortesting:mocha-core@5.2.0_3 gadicc:blaze-react-component accounts-github@1.4.3 github-config-ui@1.0.1 -ibmcloud:accounts-ghe +ibmcloud:accounts-ghe@3.0.0 useraccounts:core useraccounts:unstyled service-configuration@1.0.11 diff --git a/.meteor/versions b/.meteor/versions index 64a9156c..1dbae999 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -1,7 +1,7 @@ accounts-base@1.6.0 accounts-github@1.4.3 accounts-oauth@1.2.0 -accounts-password@1.6.0 +accounts-password@1.6.1 accounts-ui@1.3.1 accounts-ui-unstyled@1.4.2 ahref:flow-router-breadcrumb@1.1.0 @@ -51,7 +51,7 @@ hot-code-push@1.0.4 html-tools@1.0.11 htmljs@1.0.11 http@1.4.2 -ibmcloud:accounts-ghe@2.0.5 +ibmcloud:accounts-ghe@3.0.0 id-map@1.1.0 inter-process-messaging@0.1.1 johanbrook:publication-collector@1.1.0 @@ -69,7 +69,7 @@ meteor-base@1.4.0 meteortesting:browser-tests@1.0.0 meteortesting:mocha@1.1.3 meteortesting:mocha-core@5.2.0_3 -minifier-css@1.5.0 +minifier-css@1.5.1 minifier-js@2.6.0 minimongo@1.6.0 mobile-experience@1.1.0 diff --git a/README.md b/README.md index bcb87f61..07f02867 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,6 @@ When deploying RazeeDash these variables can be set by adding them to the `razee | MONGO_URL | Required | - | URL to your mongo instance | | MONGO_OPTIONS | Optional | - | Set additional mongo connection string options | | OAUTH_SECRET_KEY | Optional | - | GitHub OAuth Secret Key | -| GITHUB_URL | Optional | | Required when using GitHub Enterprise authentication | -| GITHUB_API | Optional | | Required when using GitHub Enterprise authentication | | BITBUCKET_URL | Optional | | Required when using Bitbucket Enterprise authentication | | BITBUCKET_API | Optional | | Required when using Bitbucket Enterprise authentication | | BUILD_ID | Optional | Travis build ID | Travis Build ID | @@ -52,18 +50,6 @@ Example registration for running locally. | Homepage URL | | | User authorization callback URL | | -### Using GitHub Enterprise for authentication - -- Add `github_url` and `github_api` to the `razeedash-config` ConfigMap. For example: - - ```yaml - apiVersion: v1 - kind: ConfigMap - data: - github_url: https://github.your_company.com/ - github_api: https://github.your_company.com/api/v3/ - ``` - ### Using Bitbucket Enterprise authentication Add `bitbucket_url` and `bitbucket_api` to the `razeedash-config` ConfigMap. For example: diff --git a/imports/api/lib/ghe.js b/imports/api/lib/ghe.js index 9b514944..8cc2b49b 100644 --- a/imports/api/lib/ghe.js +++ b/imports/api/lib/ghe.js @@ -19,13 +19,20 @@ import { OAuth } from 'meteor/oauth'; import { HTTP } from 'meteor/http'; import log from './log.js'; import _ from 'lodash'; -import { loginType } from './login.js'; +import { ServiceConfiguration } from 'meteor/service-configuration'; +import { loginType, sanitizeUrl } from './login.js'; function listOrgs(loggedInUserObj){ - const url = `${Meteor.settings.public.GITHUB_API}user/memberships/orgs?state=active&per_page=100`; const serviceName = loginType(); + let gitUrl = 'https://api.github.com'; + if(serviceName === 'ghe') { + let {gheURL} = ServiceConfiguration.configurations.findOne({ service: 'ghe' }, {fields: {gheURL: 1}}); + gitUrl = sanitizeUrl(gheURL) + '/api/v3'; + } + + const url = `${gitUrl}/user/memberships/orgs?state=active&per_page=100`; const token = OAuth.openSecret( loggedInUserObj.services[serviceName].accessToken, loggedInUserObj._id ); - + const options = { headers: { Authorization: `bearer ${token}`, diff --git a/imports/api/lib/login.js b/imports/api/lib/login.js index 40559931..4da52df0 100644 --- a/imports/api/lib/login.js +++ b/imports/api/lib/login.js @@ -27,6 +27,19 @@ AccountsTemplates.configure({ }, }); +const sanitizeUrl = (url) => { + + const httpCheck = /^((http|https):\/\/)/; + if(!httpCheck.test(url)) { + url = `https://${url}`; + } + + const trailingSlash = /\/*$/gi; + const newUrl = url.replace(trailingSlash, ''); + + return newUrl; +}; + // A user can logon via github, github enterprise, bitbucket or they can create a local id/password stored in mongo // `localUser` is used throughout our code so that we can skip calls to the github api for local users function localUser() { @@ -47,4 +60,4 @@ function getServiceConfiguration() { return config ? config.service : undefined; } -export { localUser, loginType, getServiceConfiguration }; +export { localUser, loginType, getServiceConfiguration, sanitizeUrl }; diff --git a/imports/startup/client/index.js b/imports/startup/client/index.js index 7c8fff67..1f4edf70 100644 --- a/imports/startup/client/index.js +++ b/imports/startup/client/index.js @@ -32,7 +32,8 @@ import { Orgs } from '/imports/api/org/orgs'; import { Clusters } from '/imports/api/cluster/clusters/clusters'; import { Session } from 'meteor/session'; import { Accounts } from 'meteor/accounts-base'; -import { localUser, loginType, getServiceConfiguration } from '/imports/api/lib/login.js'; +import { localUser, loginType, getServiceConfiguration, sanitizeUrl } from '/imports/api/lib/login.js'; +import { ServiceConfiguration } from 'meteor/service-configuration'; Accounts.ui.config( { requestPermissions: { github: ['read:user', 'read:org'], @@ -145,19 +146,16 @@ Template.registerHelper('meteorSetting', (name)=>{ return _.get(Meteor.settings.public, name, null); }); -Template.registerHelper('githubUrl', ()=>{ - return Meteor.settings.public.GITHUB_URL; -}); -Template.registerHelper('bitbucketUrl', ()=>{ - return Meteor.settings.public.BITBUCKET_URL; -}); Template.registerHelper('scmUrl', ()=>{ const service = getServiceConfiguration(); let scmLink = ''; if(service === 'bitbucket') { scmLink = Meteor.settings.public.BITBUCKET_URL; + } else if(service === 'ghe') { + let {gheURL} = ServiceConfiguration.configurations.findOne({ service: 'ghe' }, {fields: {gheURL: 1}}); + scmLink = sanitizeUrl(gheURL); } else { - scmLink = Meteor.settings.public.GITHUB_URL; + scmLink = 'https://github.com/'; } return scmLink.endsWith('/') ? scmLink : scmLink + '/'; }); @@ -292,12 +290,6 @@ Template.registerHelper('outputDisabledFlipped', (disabled) => { return (disabled ? '' : 'disabled'); }); -Template.registerHelper('commitHref', (commitId, deployment=null)=>{ - const githubOrgName = Session.get('currentOrgName'); - const githubProjName = deployment.searchableData.name; - return `${Meteor.settings.public.GITHUB_URL}${githubOrgName}/${githubProjName}/commit/${commitId}`; -}); - Template.registerHelper('valuesJoined', (values) => { if (values.length == 1) { return `"${values[0]}"`; diff --git a/imports/startup/server/index.js b/imports/startup/server/index.js index ece303bb..9971cab2 100644 --- a/imports/startup/server/index.js +++ b/imports/startup/server/index.js @@ -80,8 +80,6 @@ const migrateUserOrgs = () => { Meteor.startup(()=>{ // envs copied over to client - Meteor.settings.public.GITHUB_URL = process.env.GITHUB_URL || 'https://github.com/'; - Meteor.settings.public.GITHUB_API = process.env.GITHUB_API || 'https://api.github.com/'; Meteor.settings.public.BITBUCKET_URL = process.env.BITBUCKET_URL || 'https://bitbucket.org/'; Meteor.settings.public.BITBUCKET_API = process.env.BITBUCKET_API || 'https://api.bitbucket.org/2.0/'; Meteor.settings.public.RAZEE_GITHUB_URL = 'https://github.com/razee-io/razeedash'; @@ -93,7 +91,7 @@ Meteor.startup(()=>{ }; versionInfo.str = `${versionInfo.buildId}_${versionInfo.lastCommitId}`; Meteor.settings.public.version = versionInfo; - + if ( process.env.OAUTH_SECRET_KEY ) { migrateUnencryptedUsers(); } diff --git a/imports/ui/pages/razeeWelcome/index.js b/imports/ui/pages/razeeWelcome/index.js index 980a2df7..57cb8d20 100644 --- a/imports/ui/pages/razeeWelcome/index.js +++ b/imports/ui/pages/razeeWelcome/index.js @@ -85,6 +85,9 @@ Template.SelectOrg.events({ Template.SelectOrg_git.helpers({ scmIcon() { return loginType() === 'bitbucket' ? 'fa-bitbucket' : 'fa-github'; + }, + orgName() { + return Template.currentData().org.name; } }); diff --git a/imports/ui/pages/razeeWelcome/page.html b/imports/ui/pages/razeeWelcome/page.html index d06d6bc5..6d87ef8b 100644 --- a/imports/ui/pages/razeeWelcome/page.html +++ b/imports/ui/pages/razeeWelcome/page.html @@ -187,5 +187,5 @@ diff --git a/kubernetes/razeedash/resource.yaml b/kubernetes/razeedash/resource.yaml index 748528ea..81056656 100644 --- a/kubernetes/razeedash/resource.yaml +++ b/kubernetes/razeedash/resource.yaml @@ -55,18 +55,6 @@ items: name: razeedash-secret key: oauth_secret_key optional: true - - name: GITHUB_URL - valueFrom: - configMapKeyRef: - name: razeedash-config - key: github_url - optional: true - - name: GITHUB_API - valueFrom: - configMapKeyRef: - name: razeedash-config - key: github_api - optional: true - name: BITBUCKET_URL valueFrom: configMapKeyRef: diff --git a/package-lock.json b/package-lock.json index 6a9f76b9..1cd2637a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -308,9 +308,9 @@ "dev": true }, "@types/node": { - "version": "14.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.10.tgz", - "integrity": "sha512-Bz23oN/5bi0rniKT24ExLf4cK0JdvN3dH/3k0whYkdN4eI4vS2ZW/2ENNn2uxHCzWcbdHIa/GRuWQytfzCjRYw==" + "version": "14.0.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.13.tgz", + "integrity": "sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA==" }, "@types/zen-observable": { "version": "0.8.0", @@ -341,9 +341,9 @@ "dev": true }, "acorn": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", - "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz", + "integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==", "dev": true }, "acorn-jsx": { @@ -1103,9 +1103,9 @@ } }, "chalk": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", - "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", diff --git a/packages/ghe/ghe.js b/packages/ghe/ghe.js new file mode 100755 index 00000000..f8c824df --- /dev/null +++ b/packages/ghe/ghe.js @@ -0,0 +1,21 @@ +/* eslint-disable no-undef */ + +Accounts.oauth.registerService('ghe'); + +if (Meteor.isClient) { + Meteor.loginWithGhe = function(options, callback) { + // support a callback without options + if (!callback && typeof options === 'function') { + callback = options; + options = null; + } + + const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + Ghe.requestCredential(options, credentialRequestCompleteCallback); + }; +} else { + Accounts.addAutopublishFields({ + forLoggedInUser: ['services.ghe'], + forOtherUsers: ['services.ghe.username'] + }); +} diff --git a/packages/ghe/ghe_client.js b/packages/ghe/ghe_client.js new file mode 100755 index 00000000..605338f8 --- /dev/null +++ b/packages/ghe/ghe_client.js @@ -0,0 +1,57 @@ +/* eslint-disable no-undef */ + +Ghe = {}; + +// Request Github credentials for the user +// @param options {optional} +// @param credentialRequestCompleteCallback {Function} Callback function to call on +// completion. Takes one argument, credentialToken on success, or Error on +// error. +Ghe.requestCredential = function(options, credentialRequestCompleteCallback) { + // support both (options, callback) and (callback). + if (!credentialRequestCompleteCallback && typeof options === 'function') { + credentialRequestCompleteCallback = options; + options = {}; + } + + const config = ServiceConfiguration.configurations.findOne({ service: 'ghe' }); + if (!config) { + credentialRequestCompleteCallback && credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError()); + return; + } + const credentialToken = Random.secret(); + + const scope = (options && options.requestPermissions) || ['user:email']; + const flatScope = _.map(scope, encodeURIComponent).join('+'); + + const loginStyle = OAuth._loginStyle('ghe', config, options); + + let url = config.gheURL; + const httpCheck = /^((http|https):\/\/)/; + if(!httpCheck.test(config.gheURL)) { + url = `https://${config.gheURL}`; + } + + const trailingSlash = /\/*$/gi; + const gheUrl = url.replace(trailingSlash, ''); + + const loginUrl = + gheUrl + '/login/oauth/authorize' + + '?client_id=' + config.clientId + + '&scope=' + flatScope + + '&redirect_uri=' + OAuth._redirectUri('ghe', config) + + '&state=' + OAuth._stateParam(loginStyle, credentialToken, options && options.redirectUrl); + + OAuth.launchLogin({ + loginService: 'ghe', + loginStyle, + loginUrl, + credentialRequestCompleteCallback, + credentialToken, + popupOptions: { + width: 900, + height: 450 + } + }); + +}; diff --git a/packages/ghe/ghe_configure.html b/packages/ghe/ghe_configure.html new file mode 100755 index 00000000..1c25145b --- /dev/null +++ b/packages/ghe/ghe_configure.html @@ -0,0 +1,16 @@ + diff --git a/packages/ghe/ghe_configure.js b/packages/ghe/ghe_configure.js new file mode 100755 index 00000000..5246c442 --- /dev/null +++ b/packages/ghe/ghe_configure.js @@ -0,0 +1,13 @@ +/* eslint-disable no-undef */ + +Template.configureLoginServiceDialogForGhe.helpers({ + siteUrl: function() { + return Meteor.absoluteUrl(); + } +}); + +Template.configureLoginServiceDialogForGhe.fields = () => [ + { property: 'gheURL', label: 'GitHub Enterprise URL' }, + { property: 'clientId', label: 'Client ID' }, + { property: 'secret', label: 'Client Secret' } +]; diff --git a/packages/ghe/ghe_login_button.css b/packages/ghe/ghe_login_button.css new file mode 100755 index 00000000..1506bfc3 --- /dev/null +++ b/packages/ghe/ghe_login_button.css @@ -0,0 +1,3 @@ +#login-buttons-image-ghe { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wJGBYxHYxl31wAAAHpSURBVDjLpZI/aFNRFMZ/973bJqGRPopV4qNq/+SpTYnWRhCKilShg9BGcHOM+GfQoZuLk4iLgw4qZNBaHLuIdBNHl7Ta1qdNFI3SihnaNG1MpH3vuiQYQnwZvHCG893zffc751z4z6PX5T5gA1DAKnAaOAQEgAfAVeCpl+CeCrlRuEC6maO4h0A1wl4tPAHMqNUthvrDdHYY7A3t4rDVjeO6rBU2FaABM1WCrBNoi48Mi+nH9yj+KtPibAKwJXfQ5vcRG7soUnYmWEuQgAEIYBv4cGpoILI0Z4tyYYPegS6UguyijZQ6J45GSNmZHzUcJYD2ii2Ajv7efZ8WZ6ZwXFj79hXpayW4O0SL1Nl/8jzZlZ9dQLFS70pgvZKIyGD0yvu5eRmMnrk1PjI81ir1qBACTdPevXj95mVuNX8XKDQc/+T334bZZ104cvzYw2s3J3qAL5WXSsDbf61NNMBu+wOBs+VSyQ84Nfhg028ZGx3/qyy0lC7lgi7lghBitoon03lvB8l0/k7Wnk+8mny0cyXzEcfZxgwfZPTyRMHsOzAFXE9YhtNQIJnOx4FpJXT1eSkn2g0frqMoFrfoCXcqlCOAGwnLuO/l4JymcWl5uRxzXUKghBAiZ5r+WaV4lrCM555zqO+x2d0ftGmpiA/0k70AAAAASUVORK5CYII=); +} diff --git a/packages/ghe/ghe_server.js b/packages/ghe/ghe_server.js new file mode 100755 index 00000000..a3d87215 --- /dev/null +++ b/packages/ghe/ghe_server.js @@ -0,0 +1,118 @@ +/* eslint-disable no-undef */ + +Ghe = {}; + +OAuth.registerService('ghe', 2, null, function(query) { + + const accessToken = getAccessToken(query); + const identity = getIdentity(accessToken); + const emails = getEmails(accessToken); + const primaryEmail = emails.find(email => email.primary); + + return { + serviceData: { + id: identity.id, + accessToken: OAuth.sealSecret(accessToken), + email: identity.email || (primaryEmail && primaryEmail.email) || '', + username: identity.login, + emails: emails + }, + options: { profile: { name: identity.name } } + }; +}); + +const apiPath = 'api/v3'; + +// http://developer.github.com/v3/#user-agent-required +let userAgent = 'Meteor'; +if (Meteor.release) { + userAgent += '/' + Meteor.release; +} + +const getAccessToken = function(query) { + const config = ServiceConfiguration.configurations.findOne({ service: 'ghe' }); + if (!config) { + throw new ServiceConfiguration.ConfigError(); + } + + let response; + try { + const gheUrl = sanitizeUrl(config.gheURL); + response = HTTP.post(`${gheUrl}/login/oauth/access_token`, { + headers: { + Accept: 'application/json', + 'User-Agent': userAgent + }, + params: { + code: query.code, + client_id: config.clientId, + client_secret: OAuth.openSecret(config.secret), + redirect_uri: OAuth._redirectUri('ghe', config), + state: query.state + } + }); + } catch (err) { + throw Object.assign( + new Error(`Failed to complete OAuth handshake with Github Enterprise. ${err.message}`), + { response: err.response }, + ); + } + + if (response.data.error) { // if the http response was a json object with an error attribute + throw new Error('Failed to complete OAuth handshake with GitHub Enterprise. ' + response.data.error); + } else { + return response.data.access_token; + } +}; + +const getIdentity = function(accessToken) { + const config = ServiceConfiguration.configurations.findOne({ service: 'ghe' }); + if (!config) { + throw new ServiceConfiguration.ConfigError(); + } + + try { + const gheUrl = sanitizeUrl(config.gheURL); + return HTTP.get(`${gheUrl}/${apiPath}/user`, { + headers: {'User-Agent': userAgent, 'Authorization': `token ${accessToken}`}, + }).data; + } catch (err) { + throw Object.assign( + new Error(`Failed to fetch identity from Github. ${err.message}`), + { response: err.response }, + ); + } +}; + +const getEmails = function(accessToken) { + const config = ServiceConfiguration.configurations.findOne({ service: 'ghe' }); + if (!config) { + throw new ServiceConfiguration.ConfigError(); + } + + try { + const gheUrl = sanitizeUrl(config.gheURL); + return HTTP.get( `${gheUrl}/${apiPath}/user/emails`, { + headers: {'User-Agent': userAgent, 'Authorization': `token ${accessToken}`}, + }).data; + } catch (err) { + return []; + } +}; + +const sanitizeUrl = (url) => { + + const httpCheck = /^((http|https):\/\/)/; + if(!httpCheck.test(url)) { + url = `https://${url}`; + } + + const trailingSlash = /\/*$/gi; + const ghe = url.replace(trailingSlash, ''); + + return ghe; +}; + +Ghe.retrieveCredential = function(credentialToken, credentialSecret) { + return OAuth.retrieveCredential(credentialToken, credentialSecret); +}; diff --git a/packages/ghe/package.js b/packages/ghe/package.js new file mode 100755 index 00000000..c011dbd8 --- /dev/null +++ b/packages/ghe/package.js @@ -0,0 +1,30 @@ +/* eslint-disable no-undef */ + +Package.describe({ + summary: 'Github Enterprise OAuth flow', + version: '3.0.0', + name: 'ibmcloud:accounts-ghe', + documentation: null +}); + +Package.onUse(function(api) { + api.use('accounts-base', ['client', 'server']); + api.use('ecmascript'); + api.imply('accounts-base', ['client', 'server']); + api.use('accounts-oauth', ['client', 'server']); + api.use('oauth2', ['client', 'server']); + api.use('oauth', ['client', 'server']); + api.use('http', ['client', 'server']); + api.use('underscore', 'client'); + api.use('templating', 'client'); + api.use('random', 'client'); + api.use('service-configuration', ['client', 'server']); + api.addFiles('ghe_login_button.css', 'client'); + api.addFiles('ghe.js'); + api.export('Ghe'); + api.addFiles( + ['ghe_configure.html', 'ghe_configure.js'], + 'client'); + api.addFiles('ghe_server.js', 'server'); + api.addFiles('ghe_client.js', 'client'); +});