Open Source Release v1.0
|
@ -0,0 +1,28 @@
|
|||
# Node
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
|
||||
# OS X
|
||||
.DS_Store
|
||||
|
||||
# Local test files
|
||||
sync-*.js
|
||||
|
||||
# Local development environment scripts
|
||||
bin/local*
|
||||
bin/contoso
|
||||
app.yaml
|
||||
|
||||
# Tooling
|
||||
.vscode/
|
||||
|
||||
# Editors
|
||||
.vscode/
|
||||
|
||||
# Other routes and components that need not be included here
|
||||
docs/
|
||||
routes/microsoft-specific*
|
||||
routes/friends/
|
||||
routes/docs.js
|
||||
views/docs/
|
||||
public/css/docs.css
|
|
@ -0,0 +1,6 @@
|
|||
.git
|
||||
node_modules
|
||||
public/js/jquery*.*
|
||||
public/js/html5*.*
|
||||
public/js/bootstrap*.*
|
||||
public/js/timeago*.*
|
|
@ -0,0 +1,25 @@
|
|||
Azure Open Source Portal for GitHub v1.0
|
||||
|
||||
Copyright (c) Microsoft Corporation
|
||||
|
||||
All rights reserved.
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,10 @@
|
|||
## azureopensource-portal
|
||||
|
||||
The Azure Open Source Portal for GitHub is the culmination of years of trying to manage the
|
||||
Azure presence on GitHub through a lot of trial, error, and improvement in tooling.
|
||||
|
||||
## This file needs work!
|
||||
|
||||
## LICENSE
|
||||
|
||||
MIT license. See also: [LICENSE](LICENSE)
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var async = require('async');
|
||||
var DataClient = require('./data');
|
||||
var express = require('express');
|
||||
var redis = require('redis');
|
||||
var app = express();
|
||||
|
||||
// Asynchronous initialization for the Express app, configuration and data stores.
|
||||
app.initializeApplication = function init(config, callback) {
|
||||
var dc;
|
||||
var redisFirstCallback;
|
||||
var redisClient = redis.createClient(config.redis.port, config.redis.host);
|
||||
redisClient.on('connect', function () {
|
||||
if (redisFirstCallback) {
|
||||
var cb = redisFirstCallback;
|
||||
redisFirstCallback = null;
|
||||
cb();
|
||||
}
|
||||
});
|
||||
async.parallel([
|
||||
function (cb) {
|
||||
new DataClient(config, function (error, dcInstance) {
|
||||
dc = dcInstance;
|
||||
cb();
|
||||
});
|
||||
},
|
||||
function (cb) {
|
||||
redisFirstCallback = cb;
|
||||
redisClient.auth(config.redis.key);
|
||||
},
|
||||
], function (error) {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
app.set('dataclient', dc);
|
||||
dc.cleanupInTheFuture = {
|
||||
redisClient: redisClient
|
||||
};
|
||||
app.set('runtimeConfig', config);
|
||||
require('./middleware/')(app, express, config, __dirname);
|
||||
app.use('/', require('./routes/'));
|
||||
require('./middleware/error-routes')(app);
|
||||
callback(null, app);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = app;
|
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env node
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var app = require('../app');
|
||||
var debug = require('debug')('g:server');
|
||||
var http = require('http');
|
||||
|
||||
function normalizePort(val) {
|
||||
var port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var port = normalizePort(process.env.PORT || '3000');
|
||||
app.set('port', port);
|
||||
|
||||
var config = require('../configuration')(process.env);
|
||||
|
||||
app.initializeApplication(config, function (error) {
|
||||
if (error) throw error;
|
||||
|
||||
var server = http.createServer(app);
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
debug('Listening on ' + bind);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,117 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var utils = require('./utils');
|
||||
|
||||
module.exports = function translateEnvironmentToConfiguration(env) {
|
||||
if (!env) {
|
||||
env = process.env;
|
||||
}
|
||||
var i = 0;
|
||||
var pkgInfo = require('./package.json');
|
||||
var config = {
|
||||
logging: {
|
||||
errors: env.SITE_SKIP_ERRORS === undefined,
|
||||
version: pkgInfo.version,
|
||||
},
|
||||
companyName: env.COMPANY_NAME,
|
||||
serviceBanner: env.SITE_SERVICE_BANNER,
|
||||
corporate: {
|
||||
userProfilePrefix: env.CORPORATE_PROFILE_PREFIX,
|
||||
trainingResources: require('./resources.json'),
|
||||
portalAdministratorEmail: env.PORTAL_ADMIN_EMAIL,
|
||||
},
|
||||
// Friends are GitHub username(s) which have special
|
||||
// access for application use such as CLA tooling and
|
||||
// compliance/audit accounts. Supports comma-sep lists.
|
||||
friends: {
|
||||
cla: utils.arrayFromString(env.FRIENDS_CLA),
|
||||
employeeData: utils.arrayFromString(env.FRIENDS_DATA),
|
||||
},
|
||||
// GitHub application properties and secrets
|
||||
github: {
|
||||
clientId: env.GITHUB_CLIENT_ID,
|
||||
clientSecret: env.GITHUB_CLIENT_SECRET,
|
||||
callbackUrl: env.GITHUB_CALLBACK_URL,
|
||||
},
|
||||
organizations: [],
|
||||
// A salt needs to be provided to secure sessions and cookies.
|
||||
express: {
|
||||
sessionSalt: env.SESSION_SALT
|
||||
},
|
||||
// The app uses authentication with Azure Active Directory to grant access
|
||||
// to the GitHub organization.
|
||||
activeDirectory: {
|
||||
clientId: env.AAD_CLIENT_ID,
|
||||
clientSecret: env.AAD_CLIENT_SECRET,
|
||||
tenantId: env.AAD_TENANT_ID,
|
||||
redirectUrl: env.AAD_REDIRECT_URL,
|
||||
allowTenantGuests: (env.AAD_ALLOW_TENANT_GUESTS && env.AAD_ALLOW_TENANT_GUESTS == 'allow')
|
||||
},
|
||||
// AppInsights is a Microsoft Cloud product for gathering analytics and
|
||||
// other useful information about apps. This app uses the Node.js npm
|
||||
// module for app insights to gather information on server generation
|
||||
// times, while the client JavaScript wrapper for AppInsights is also
|
||||
// used for monitoring client browser attributes and information. If the
|
||||
// key is not supplied, the app continues functioning.
|
||||
applicationInsights: {
|
||||
instrumentationKey: env.APPINSIGHTS_INSTRUMENTATION_KEY
|
||||
},
|
||||
// An Azure storage account is used as all data is stored in a
|
||||
// geo-replicated storage account in table store. This is simple
|
||||
// model vs a SQL Database instance, but requires doing joins
|
||||
// on the server.
|
||||
azureStorage: {
|
||||
account: env.XSTORE_ACCOUNT,
|
||||
key: env.XSTORE_KEY,
|
||||
prefix: env.XSTORE_PREFIX
|
||||
},
|
||||
// Redis is used for shared session state across running site instances.
|
||||
// The Azure Redis offering includes a redundant option, but as the
|
||||
// session store is designed like a cache, the only outcome of lost
|
||||
// Redis data is that the user will need to sign in again.
|
||||
redis: {
|
||||
port: env.REDIS_PORT,
|
||||
host: env.REDIS_HOST,
|
||||
key: env.REDIS_KEY,
|
||||
ttl: env.REDIS_TTL || (60 * 60 * 24 * 7 /* one week */),
|
||||
prefix: env.REDIS_PREFIX,
|
||||
},
|
||||
};
|
||||
for (i = 1; env['GITHUB_ORG' + i + '_NAME']; i++) {
|
||||
var prefix = 'GITHUB_ORG' + i + '_';
|
||||
var org = {
|
||||
name: env[prefix + 'NAME'],
|
||||
type: env[prefix + 'TYPE'] || 'public',
|
||||
ownerToken: env[prefix + 'TOKEN'],
|
||||
notificationRepo: env[prefix + 'NOTIFICATION_REPO'],
|
||||
teamAllMembers: env[prefix + 'EVERYONE_TEAMID'],
|
||||
teamRepoApprovers: env[prefix + 'REPO_APPROVERS_TEAMID'],
|
||||
hookSecrets: utils.arrayFromString(env[prefix + 'HOOK_TOKENS']),
|
||||
teamAllRepos: env[prefix + 'SECURITY_TEAMID'],
|
||||
teamAllRepoWriteId: env[prefix + 'ALLREPOWRITE_TEAMID'],
|
||||
teamSudoers: env[prefix + 'SUDOERS_TEAMID'],
|
||||
description: env[prefix + 'DESCRIPTION'],
|
||||
highlightedTeams: [],
|
||||
};
|
||||
if (i == 1) {
|
||||
org.teamPortalSudoers = env[prefix + 'PORTAL_SUDOERS_TEAMID'];
|
||||
}
|
||||
var highlightIds = utils.arrayFromString(env[prefix + 'HIGHLIGHTED_TEAMS']);
|
||||
var highlightText = utils.arrayFromString(env[prefix + 'HIGHLIGHTED_TEAMS_INFO'], ';');
|
||||
if (highlightIds.length === highlightText.length) {
|
||||
for (var j = 0; j < highlightIds.length; j++) {
|
||||
org.highlightedTeams.push({
|
||||
id: highlightIds[j],
|
||||
description: highlightText[j],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error('Invalid matching of size for highlighted teams.');
|
||||
}
|
||||
config.organizations.push(org);
|
||||
}
|
||||
return config;
|
||||
};
|
|
@ -0,0 +1,639 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
// This is the original data interface for this portal. It uses Azure
|
||||
// table storage and its Node.js SDK. There's a lot of potential work
|
||||
// to do here to better factor this and allow for other data providers
|
||||
// such as MonogDB...
|
||||
|
||||
var azure = require('azure-storage');
|
||||
var async = require('async');
|
||||
var uuid = require('node-uuid');
|
||||
var os = require('os');
|
||||
|
||||
var staticHostname = os.hostname().toString();
|
||||
|
||||
function DataClient (config, callback) {
|
||||
var storageAccountName = config.azureStorage.account;
|
||||
var storageAccountKey = config.azureStorage.key;
|
||||
var prefix = config.azureStorage.prefix;
|
||||
this.table = azure.createTableService(storageAccountName, storageAccountKey);
|
||||
this.entGen = azure.TableUtilities.entityGenerator;
|
||||
this.options = {
|
||||
partitionKey: prefix + 'pk',
|
||||
linksTableName: prefix + 'links',
|
||||
pendingApprovalsTableName: prefix + 'pending',
|
||||
errorsTableName: prefix + 'errors',
|
||||
auditTableName: prefix + 'auditlog',
|
||||
};
|
||||
var dc = this;
|
||||
var tableNames = [
|
||||
dc.options.linksTableName,
|
||||
dc.options.pendingApprovalsTableName,
|
||||
dc.options.errorsTableName,
|
||||
dc.options.auditTableName,
|
||||
];
|
||||
async.each(tableNames, function (tableName, callback) {
|
||||
dc.table.createTableIfNotExists(tableName, callback);
|
||||
}, function (error) {
|
||||
if (callback) {
|
||||
return callback(error, dc);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Strip the Azure table storage fields if no uninteresting fields are provided.
|
||||
var reduceEntity = function reduceEntity(instance, uninterestingFields) {
|
||||
if (instance === undefined || instance === null) {
|
||||
return instance;
|
||||
}
|
||||
if (uninterestingFields === undefined) {
|
||||
uninterestingFields = ['.metadata', 'Timestamp', 'RowKey', 'PartitionKey'];
|
||||
}
|
||||
if (uninterestingFields && uninterestingFields.length) {
|
||||
for (var i = 0; i < uninterestingFields.length; i++) {
|
||||
if (instance[uninterestingFields[i]] !== undefined) {
|
||||
delete instance[uninterestingFields[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var column in instance) {
|
||||
if (instance[column] && instance[column]._ !== undefined) {
|
||||
instance[column] = instance[column]._;
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
};
|
||||
|
||||
DataClient.prototype.reduceEntity = reduceEntity;
|
||||
|
||||
DataClient.prototype.requestToUserInformation = function rtui(req, storeFullUserInformation) {
|
||||
var info = {
|
||||
ghid: undefined,
|
||||
ghu: undefined,
|
||||
aad: undefined,
|
||||
fulluser: undefined
|
||||
};
|
||||
if (storeFullUserInformation) {
|
||||
info.fulluser = JSON.stringify(req.user);
|
||||
}
|
||||
if (req && req.user && req.user.github && req.user.github.id) {
|
||||
info.ghid = req.user.github.id;
|
||||
if (info.ghid.toString) {
|
||||
info.ghid = info.ghid.toString();
|
||||
}
|
||||
if (req.user.github.username) {
|
||||
info.ghu = req.user.github.username;
|
||||
}
|
||||
}
|
||||
if (req && req.user && req.user.azure && req.user.azure.username) {
|
||||
info.aad = req.user.azure.username;
|
||||
}
|
||||
return info;
|
||||
};
|
||||
|
||||
DataClient.prototype.insertErrorLogEntry = function insertErrorEntry(version, req, err, meta, callback) {
|
||||
// generic configuration, should move out at some point...
|
||||
var storeFullUserInformation = false;
|
||||
var storeUnknownUserErrors = false;
|
||||
var storeRequestInformation = true;
|
||||
var cbNoErrors = function(callback) {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
var dc = this;
|
||||
var entity;
|
||||
// (PartitionKey, RowKey): (ghid || 0, new uuid)
|
||||
// (ghu, ghid, aad): user information
|
||||
// (t, cid): (time when method called, correlation ID)
|
||||
// (e, json, meta): (error message, JSON serialized err, JSON metadata)
|
||||
// (url, host, ...): various host and request informational fields
|
||||
try
|
||||
{
|
||||
var info = dc.requestToUserInformation(req, storeFullUserInformation);
|
||||
// We may encounter users without a session. In these cases, we could log with -1 ID for pkey (OR use correlation ID for the pkey... hmm.)
|
||||
if (info.ghid === undefined) {
|
||||
if (!storeUnknownUserErrors) {
|
||||
return cbNoErrors(callback);
|
||||
}
|
||||
info.ghid = -1;
|
||||
}
|
||||
info.v = version;
|
||||
if (req.headers && req.headers.referer) {
|
||||
info.referer = req.headers.referer;
|
||||
}
|
||||
var partitionKey = info.ghid;
|
||||
var uniqueErrorId = uuid.v4();
|
||||
entity = dc.createEntity(partitionKey, uniqueErrorId, info);
|
||||
var errorMessage = 'The error object was undefined.';
|
||||
var errorJson;
|
||||
var errorStack;
|
||||
var errorStatus = '200';
|
||||
if (err) {
|
||||
// If err.meta is set, use that for the metadata up-level, and remove from err object.
|
||||
if (err.meta && ! meta) {
|
||||
meta = err.meta;
|
||||
delete err.meta;
|
||||
}
|
||||
errorStack = err.stack;
|
||||
if (err.status) {
|
||||
errorStatus = err.status;
|
||||
// delete err.status; // ? may not want to do this...
|
||||
}
|
||||
if (err.message) {
|
||||
errorMessage = err.message;
|
||||
} else {
|
||||
if (err.toString) {
|
||||
errorMessage = err.toString();
|
||||
} else {
|
||||
errorMessage = 'The provided error instance is not a string and has no toString method.';
|
||||
}
|
||||
}
|
||||
try {
|
||||
errorJson = JSON.stringify(err);
|
||||
} catch (je) {
|
||||
// Ignore any serialization errors or circular reference problems, the rest will still be logged in this case.
|
||||
}
|
||||
}
|
||||
var metaJson;
|
||||
if (meta) {
|
||||
try {
|
||||
metaJson = JSON.stringify(meta);
|
||||
} catch (je) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
var errorEntity = {
|
||||
t: new Date().getTime(),
|
||||
cid: (req && req.correlationId ? req.correlationId : undefined),
|
||||
e: errorMessage,
|
||||
stack: errorStack,
|
||||
json: errorJson,
|
||||
meta: metaJson,
|
||||
status: errorStatus,
|
||||
'new': true
|
||||
};
|
||||
dc.mergeIntoEntity(entity, errorEntity);
|
||||
if (storeRequestInformation) {
|
||||
var sri = {
|
||||
url: req.scrubbedUrl || req.originalUrl || req.url,
|
||||
ua: req.headers['user-agent'],
|
||||
host: staticHostname
|
||||
};
|
||||
dc.mergeIntoEntity(entity, sri);
|
||||
}
|
||||
} catch (ex) {
|
||||
// Retry policy could be nice, OR log this separately if possible. Streaming logs will store this for now at least.
|
||||
console.dir(ex);
|
||||
return cbNoErrors(callback);
|
||||
}
|
||||
if (entity) {
|
||||
dc.table.insertEntity(dc.options.errorsTableName, entity, function (error, xy) {
|
||||
if (error) {
|
||||
// CONSIDER: Replace console with debug call for morgan
|
||||
console.dir(error);
|
||||
}
|
||||
cbNoErrors(callback);
|
||||
});
|
||||
} else {
|
||||
cbNoErrors(callback);
|
||||
}
|
||||
};
|
||||
|
||||
DataClient.prototype.updateError = function (partitionKey, rowKey, mergeEntity, callback) {
|
||||
var dc = this;
|
||||
var entity = dc.createEntity(partitionKey, rowKey, mergeEntity);
|
||||
dc.table.mergeEntity(dc.options.errorsTableName, entity, callback);
|
||||
};
|
||||
|
||||
DataClient.prototype.removeError = function (partitionKey, rowKey, callback) {
|
||||
var dc = this;
|
||||
dc.table.deleteEntity(dc.options.errorsTableName, dc.createEntity(partitionKey, rowKey), callback);
|
||||
};
|
||||
|
||||
DataClient.prototype.getActiveErrors = function (correlationId, callback) {
|
||||
var dc = this;
|
||||
// Correlation ID is optional
|
||||
if (typeof(correlationId) == 'function') {
|
||||
callback = correlationId;
|
||||
correlationId = undefined;
|
||||
}
|
||||
var metadataFieldsToSkip = ['.metadata', 'Timestamp'];
|
||||
var done = false;
|
||||
var continuationToken = null;
|
||||
var entries = [];
|
||||
async.whilst(
|
||||
function () { return !done; },
|
||||
function (asyncCallback) {
|
||||
var query = new azure.TableQuery()
|
||||
.where('new eq ?', true);
|
||||
if (correlationId) {
|
||||
query.and.apply(query, ['cid eq ?', correlationId]);
|
||||
}
|
||||
dc.table.queryEntities(dc.options.errorsTableName, query, continuationToken, function (error, results) {
|
||||
if (error) {
|
||||
done = true;
|
||||
return asyncCallback(error);
|
||||
}
|
||||
if (results.continuationToken) {
|
||||
continuationToken = results.continuationToken;
|
||||
console.log('getErrors continuationToken');
|
||||
console.dir(continuationToken);
|
||||
} else {
|
||||
done = true;
|
||||
}
|
||||
if (results && results.entries && results.entries.length) {
|
||||
for (var i = 0; i < results.entries.length; i++) {
|
||||
entries.push(reduceEntity(results.entries[i], metadataFieldsToSkip));
|
||||
}
|
||||
}
|
||||
asyncCallback();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
async.sortBy(entries, function (entity, scb) {
|
||||
var t;
|
||||
var err = null;
|
||||
try {
|
||||
t = Math.round(entity.t) * -1;
|
||||
}
|
||||
catch (trx) {
|
||||
err = trx;
|
||||
}
|
||||
return scb(err, t);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
DataClient.prototype.mergeIntoEntity = function mit(entity, obj, callback) {
|
||||
var dc = this;
|
||||
if (obj) {
|
||||
for (var key in obj) {
|
||||
if (obj[key] === undefined) {
|
||||
// Skip undefined objects, including the key
|
||||
} else if (obj[key] === true) {
|
||||
entity[key] = dc.entGen.Boolean(true);
|
||||
} else if (obj[key] === false) {
|
||||
entity[key] = dc.entGen.Boolean(false);
|
||||
} else {
|
||||
// CONSIDER: Richer merging opportunities!
|
||||
if (obj[key].toString) {
|
||||
entity[key] = dc.entGen.String(obj[key].toString());
|
||||
} else {
|
||||
entity[key] = dc.entGen.String(obj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (callback) {
|
||||
callback(null, entity);
|
||||
} else {
|
||||
return entity;
|
||||
}
|
||||
};
|
||||
|
||||
DataClient.prototype.createEntity = function ce(partitionKey, rowKey, obj, callback) {
|
||||
var dc = this;
|
||||
if (typeof(obj) == 'function') {
|
||||
callback = obj;
|
||||
obj = undefined;
|
||||
}
|
||||
var entity = {
|
||||
PartitionKey: dc.entGen.String(partitionKey),
|
||||
RowKey: dc.entGen.String(rowKey)
|
||||
};
|
||||
if (obj) {
|
||||
dc.mergeIntoEntity(entity, obj);
|
||||
}
|
||||
if (callback) {
|
||||
callback(null, entity);
|
||||
} else {
|
||||
return entity;
|
||||
}
|
||||
};
|
||||
|
||||
// links
|
||||
// -----
|
||||
// CONSIDER: Replace link calls with reduced entity "association" calls, then depre. & remove these funcs.
|
||||
DataClient.prototype.createLinkObjectFromRequest = function createLinkObject(req, callback) {
|
||||
if (req && req.user && req.user.github && req.user.azure && req.user.github.username && req.user.github.id && req.user.azure.username && req.user.azure.oid) {
|
||||
return callback(null, {
|
||||
ghu: req.user.github.username,
|
||||
ghid: req.user.github.id.toString(),
|
||||
aadupn: req.user.azure.username,
|
||||
aadname: req.user.azure.displayName,
|
||||
aadoid: req.user.azure.oid,
|
||||
joined: new Date().getTime()
|
||||
});
|
||||
} else {
|
||||
return callback(new Error('Not all fields needed for creating a link are available and authenticated. This may be a temporary problem or an implementation bug.'));
|
||||
}
|
||||
};
|
||||
|
||||
DataClient.prototype.getUserLinks = function gul(users, callback) {
|
||||
var dc = this;
|
||||
var query = new azure.TableQuery()
|
||||
.where('PartitionKey eq ?', this.options.partitionKey);
|
||||
if (!(users && users.length && users.length > 0)) {
|
||||
return callback(new Error('Must include an array of GitHub user IDs, and at least one in that array.'));
|
||||
}
|
||||
var clauses = [];
|
||||
if (users.length > 250) {
|
||||
console.log('Warning: getUserLinks called with ' + users.length + ' user IDs, which may be too many.');
|
||||
}
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
clauses.push('ghid eq ?string?');
|
||||
}
|
||||
var args = [clauses.join(' or ')].concat(users);
|
||||
query.and.apply(query, args);
|
||||
dc.table.queryEntities(dc.options.linksTableName,
|
||||
query,
|
||||
null,
|
||||
function(error, results, headers) {
|
||||
if (error) {
|
||||
error.headers = headers;
|
||||
return callback(error);
|
||||
}
|
||||
var entries = [];
|
||||
if (results && results.entries && results.entries.length) {
|
||||
for (var i = 0; i < results.entries.length; i++) {
|
||||
entries.push(reduceEntity(results.entries[i]));
|
||||
}
|
||||
}
|
||||
async.sortBy(entries, function (user, sortCallback) {
|
||||
var value = user.aadupn || user.aadname || user.ghu || user.ghid;
|
||||
if (value.toLowerCase) {
|
||||
value = value.toLowerCase();
|
||||
}
|
||||
sortCallback(null, value);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
DataClient.prototype.getUserLinkByUsername = function gulbyu(githubUsername, callback) {
|
||||
this.getUserLinkByProperty('ghu', githubUsername, function (error, data) {
|
||||
if (error) return callback(error);
|
||||
if (data && data.length) {
|
||||
if (data.length == 1) {
|
||||
callback(null, data[0]);
|
||||
} else {
|
||||
if (data.length === 0) {
|
||||
callback(null, false);
|
||||
} else {
|
||||
callback(new Error('Multiple entries returned. The data may be consistent. Please file a bug.'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
callback(new Error('No results.'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
DataClient.prototype.updateLink = function updl(userid, mergeEntity, callback) {
|
||||
var dc = this;
|
||||
var entity = dc.createEntity(dc.options.partitionKey, userid, mergeEntity);
|
||||
dc.table.mergeEntity(dc.options.linksTableName, entity, callback);
|
||||
};
|
||||
|
||||
DataClient.prototype.getUserByAadUpn = function gubauapn(employeeAlias, callback) {
|
||||
this.getUserLinkByProperty('aadupn', employeeAlias.toLowerCase(), callback);
|
||||
};
|
||||
|
||||
DataClient.prototype.getUserLinkByProperty = function gulbprop(propertyName, value, callback) {
|
||||
var dc = this;
|
||||
var query = new azure.TableQuery()
|
||||
.where(propertyName + ' eq ?', value);
|
||||
dc.table.queryEntities(dc.options.linksTableName,
|
||||
query,
|
||||
null,
|
||||
function(error, results) {
|
||||
if (error) return callback(error);
|
||||
var entries = [];
|
||||
if (results && results.entries && results.entries.length) {
|
||||
for (var i = 0; i < results.entries.length; i++) {
|
||||
entries.push(reduceEntity(results.entries[i]));
|
||||
}
|
||||
}
|
||||
callback(null, entries);
|
||||
});
|
||||
};
|
||||
|
||||
DataClient.prototype.getLink = function getLink(githubId, callback) {
|
||||
var dc = this;
|
||||
if (githubId === undefined) {
|
||||
return callback(new Error('The GitHub ID is undefined.'));
|
||||
}
|
||||
if (typeof githubId != 'string') {
|
||||
githubId = githubId.toString();
|
||||
}
|
||||
dc.table.retrieveEntity(dc.options.linksTableName, dc.options.partitionKey, githubId, function (error, result, response) {
|
||||
if (error && !result) {
|
||||
return callback(null, false);
|
||||
}
|
||||
return callback(error, result, response);
|
||||
});
|
||||
};
|
||||
|
||||
DataClient.prototype.getAllEmployees = function getAllEmployees(callback) {
|
||||
var dc = this;
|
||||
var pageSize = 500;
|
||||
var employees = [];
|
||||
var done = false;
|
||||
var continuationToken = null;
|
||||
async.whilst(
|
||||
function areWeDone() { return !done; },
|
||||
function grabPage(cb) {
|
||||
var query = new azure.TableQuery()
|
||||
.select(['aadupn', 'ghu', 'ghid'])
|
||||
.top(pageSize);
|
||||
dc.table.queryEntities(dc.options.linksTableName, query, continuationToken, function (error, results) {
|
||||
if (error) {
|
||||
done = true;
|
||||
return cb(error);
|
||||
}
|
||||
if (results.continuationToken) {
|
||||
continuationToken = results.continuationToken;
|
||||
} else {
|
||||
done = true;
|
||||
}
|
||||
if (results && results.entries && results.entries.length) {
|
||||
for (var i = 0; i < results.entries.length; i++) {
|
||||
employees.push(reduceEntity(results.entries[i]));
|
||||
}
|
||||
}
|
||||
cb();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
async.sortBy(employees, function (person, sortCallback) {
|
||||
if (person.aadupn && person.aadupn.toLowerCase) {
|
||||
person.aadupn = person.aadupn.toLowerCase();
|
||||
}
|
||||
sortCallback(null, person.aadupn);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
// 9/4/15 jwilcox: insertLink and updateLink now use the shared merge/copy entity calls. Previously these 2 methods instead did a string-only entGen and copy of key/values, so beware any behavior changes. Remove this line once happy with that.
|
||||
DataClient.prototype.insertLink = function insertLink(githubId, details, callback) {
|
||||
var dc = this;
|
||||
if (githubId === undefined) {
|
||||
return callback(new Error('The GitHub ID is undefined.'));
|
||||
}
|
||||
if (typeof githubId != 'string') {
|
||||
githubId = githubId.toString();
|
||||
}
|
||||
var entity = dc.createEntity(dc.options.partitionKey, githubId, details);
|
||||
dc.table.insertEntity(dc.options.linksTableName, entity, callback);
|
||||
};
|
||||
|
||||
DataClient.prototype.updateLink = function insertLink(githubId, details, callback) {
|
||||
var dc = this;
|
||||
if (githubId === undefined) {
|
||||
return callback(new Error('The GitHub ID is undefined.'));
|
||||
}
|
||||
if (typeof githubId != 'string') {
|
||||
githubId = githubId.toString();
|
||||
}
|
||||
var entity = dc.createEntity(dc.options.partitionKey, githubId, details);
|
||||
dc.table.mergeEntity(dc.options.linksTableName, entity, callback);
|
||||
};
|
||||
|
||||
DataClient.prototype.removeLink = function removeLink(githubId, callback) {
|
||||
var dc = this;
|
||||
if (githubId === undefined) {
|
||||
return callback(new Error('The GitHub ID is undefined.'));
|
||||
}
|
||||
if (typeof githubId != 'string') {
|
||||
githubId = githubId.toString();
|
||||
}
|
||||
dc.table.deleteEntity(dc.options.linksTableName, dc.createEntity(dc.options.partitionKey, githubId), callback);
|
||||
};
|
||||
|
||||
// pending approvals workflow
|
||||
// --------------------------
|
||||
DataClient.prototype.getPendingApprovals = function getPendingApprovals(teamsIn, callback) {
|
||||
var dc = this;
|
||||
var teams = null;
|
||||
var i;
|
||||
if (typeof teamsIn == 'number') {
|
||||
teams = [teamsIn.toString()];
|
||||
}
|
||||
else if (typeof teamsIn == 'string') {
|
||||
teams = [teamsIn];
|
||||
} else if (typeof teamsIn == 'function') {
|
||||
callback = teamsIn;
|
||||
teams = []; // Special case: empty list means all pending approvals
|
||||
} else {
|
||||
if (!(teamsIn && teamsIn.length)) {
|
||||
throw new Error('Unknown "teams" type for getPendingApprovals. Please file a bug.');
|
||||
}
|
||||
// New permissions system refactoring...
|
||||
if (teamsIn.length > 0 && teamsIn[0] && teamsIn[0].id) {
|
||||
teams = [];
|
||||
for (i = 0; i < teamsIn.length; i++) {
|
||||
teams.push(teamsIn[i].id);
|
||||
}
|
||||
}
|
||||
}
|
||||
var query = new azure.TableQuery()
|
||||
.where('PartitionKey eq ?', this.options.partitionKey)
|
||||
.and('active eq ?', true);
|
||||
if (teams.length > 0) {
|
||||
var clauses = [];
|
||||
for (i = 0; i < teams.length; i++) {
|
||||
clauses.push('teamid eq ?string?');
|
||||
}
|
||||
var args = [clauses.join(' or ')].concat(teams);
|
||||
query.and.apply(query, args);
|
||||
}
|
||||
dc.table.queryEntities(dc.options.pendingApprovalsTableName,
|
||||
query,
|
||||
null,
|
||||
function(error, results) {
|
||||
if (error) return callback(error);
|
||||
var entries = [];
|
||||
if (results && results.entries && results.entries.length) {
|
||||
for (var i = 0; i < results.entries.length; i++) {
|
||||
var r = results.entries[i];
|
||||
if (r && r.active && r.active._) {
|
||||
entries.push(reduceEntity(r, ['.metadata']));
|
||||
}
|
||||
}
|
||||
}
|
||||
callback(null, entries);
|
||||
});
|
||||
};
|
||||
|
||||
DataClient.prototype.insertApprovalRequest = function iar(teamid, details, callback) {
|
||||
var dc = this;
|
||||
if (typeof teamid != 'string') {
|
||||
teamid = teamid.toString();
|
||||
}
|
||||
details.teamid = teamid;
|
||||
dc.insertGeneralApprovalRequest('joinTeam', details, callback);
|
||||
};
|
||||
|
||||
DataClient.prototype.insertGeneralApprovalRequest = function igar(ticketType, details, callback) {
|
||||
var dc = this;
|
||||
var id = uuid.v4();
|
||||
var entity = dc.createEntity(dc.options.partitionKey, id, {
|
||||
tickettype: ticketType
|
||||
});
|
||||
dc.mergeIntoEntity(entity, details);
|
||||
dc.table.insertEntity(dc.options.pendingApprovalsTableName, entity, function (error, result, response) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
// Pass back the generated request ID first.
|
||||
callback(null, id, result, response);
|
||||
});
|
||||
};
|
||||
|
||||
DataClient.prototype.getApprovalRequest = function gar(requestId, callback) {
|
||||
var dc = this;
|
||||
dc.table.retrieveEntity(dc.options.pendingApprovalsTableName, dc.options.partitionKey, requestId, function (error, ent) {
|
||||
if (error) return callback(error);
|
||||
callback(null, reduceEntity(ent, ['.metadata']));
|
||||
});
|
||||
};
|
||||
|
||||
DataClient.prototype.getPendingApprovalsForUserId = function gpeaf(githubid, callback) {
|
||||
var dc = this;
|
||||
if (typeof githubid == 'number') {
|
||||
githubid = githubid.toString();
|
||||
}
|
||||
var query = new azure.TableQuery()
|
||||
.where('PartitionKey eq ?', this.options.partitionKey)
|
||||
.and('active eq ?', true)
|
||||
.and('ghid eq ?', githubid);
|
||||
dc.table.queryEntities(dc.options.pendingApprovalsTableName,
|
||||
query,
|
||||
null,
|
||||
function(error, results) {
|
||||
if (error) return callback(error);
|
||||
var entries = [];
|
||||
if (results && results.entries && results.entries.length) {
|
||||
for (var i = 0; i < results.entries.length; i++) {
|
||||
var r = results.entries[i];
|
||||
if (r && r.active && r.active._) {
|
||||
entries.push(reduceEntity(r, ['.metadata']));
|
||||
}
|
||||
}
|
||||
}
|
||||
callback(null, entries);
|
||||
});
|
||||
};
|
||||
|
||||
DataClient.prototype.updateApprovalRequest = function uar(requestId, mergeEntity, callback) {
|
||||
var dc = this;
|
||||
var entity = dc.createEntity(dc.options.partitionKey, requestId, mergeEntity);
|
||||
dc.table.mergeEntity(dc.options.pendingApprovalsTableName, entity, callback);
|
||||
};
|
||||
|
||||
module.exports = DataClient;
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Application Insights integration
|
||||
// ----------------------------------------------------------------------------
|
||||
module.exports = function initializeAppInsights(config) {
|
||||
if (config.applicationInsights.instrumentationKey) {
|
||||
var AppInsights = require('applicationinsights');
|
||||
var appInsights = new AppInsights({
|
||||
instrumentationKey: config.applicationInsights.instrumentationKey
|
||||
});
|
||||
appInsights.trackAllHttpServerRequests('favicon');
|
||||
appInsights.trackAllUncaughtExceptions();
|
||||
}
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var uuid = require('node-uuid');
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Generate a correlation ID
|
||||
// ----------------------------------------------------------------------------
|
||||
module.exports = function (req, res, next) {
|
||||
req.correlationId = uuid.v4();
|
||||
next();
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
module.exports = function configureErrorRoutes(app) {
|
||||
app.use(function(req, res, next) {
|
||||
var err = new Error('Not Found');
|
||||
err.status = 404;
|
||||
err.skipLog = true;
|
||||
next(err);
|
||||
});
|
||||
app.use(require('./errorHandler'));
|
||||
};
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var uuid = require('node-uuid');
|
||||
|
||||
module.exports = function(err, req, res, next) {
|
||||
var config = null;
|
||||
var errorStatus = err && err.status ? err.status : undefined;
|
||||
if (req && req.app && req.app.settings && req.app.settings.dataclient && req.app.settings.runtimeConfig) {
|
||||
config = req.app.settings.runtimeConfig;
|
||||
var version = config && config.logging && config.logging.version ? config.logging.version: '?';
|
||||
var dc = req.app.settings.dataclient;
|
||||
if (config.logging.errors && err.status !== 403 && err.skipLog !== true) {
|
||||
dc.insertErrorLogEntry(version, req, err);
|
||||
}
|
||||
}
|
||||
if (err !== undefined && err.skipLog !== true) {
|
||||
console.log('Error: ' + (err && err.message ? err.message : 'Error is undefined.'));
|
||||
if (err.stack) {
|
||||
console.error(err.stack);
|
||||
}
|
||||
if (err.innerError) {
|
||||
var inner = err.innerError;
|
||||
console.log('Inner: ' + inner.message);
|
||||
if (inner.stack) {
|
||||
console.log(inner.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Bubble OAuth errors to the forefront... this is the rate limit scenario.
|
||||
if (err && err.oauthError && err.oauthError.statusCode && err.oauthError.statusCode && err.oauthError.data) {
|
||||
var detailed = err.message;
|
||||
err = err.oauthError;
|
||||
err.status = err.statusCode;
|
||||
var data = JSON.parse(err.data);
|
||||
if (data && data.message) {
|
||||
err.message = err.statusCode + ': ' + data.message;
|
||||
} else {
|
||||
err.message = err.statusCode + ' Unauthorized received. You may have exceeded your GitHub API rate limit or have an invalid auth token at this time.';
|
||||
}
|
||||
err.detailed = detailed;
|
||||
}
|
||||
// Don't leak the Redis connection information.
|
||||
if (err && err.message && err.message.indexOf('Redis connection') >= 0 && err.message.indexOf('ETIMEDOUT')) {
|
||||
err.message = 'The session store was temporarily unavailable. Please try again.';
|
||||
err.detailed = 'Azure Redis Cache';
|
||||
}
|
||||
if (res.headersSent) {
|
||||
console.error('Headers were already sent.');
|
||||
return next(err);
|
||||
}
|
||||
res.status(err.status || 500);
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
serviceBanner: config && config.serviceBanner ? config.serviceBanner : undefined,
|
||||
detailed: err && err.detailed ? err.detailed : undefined,
|
||||
errorFancyLink: err && err.fancyLink ? err.fancyLink : undefined,
|
||||
errorStatus: errorStatus,
|
||||
error: {},
|
||||
title: err.status === 404 ? 'Not Found' : 'Oops',
|
||||
user: req.user,
|
||||
config: config,
|
||||
});
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var path = require('path');
|
||||
var favicon = require('serve-favicon');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var bodyParser = require('body-parser');
|
||||
var compression = require('compression');
|
||||
|
||||
module.exports = function initMiddleware(app, express, config, dirname) {
|
||||
require('./appInsights')(config);
|
||||
|
||||
app.set('views', path.join(dirname, 'views'));
|
||||
app.set('view engine', 'jade');
|
||||
app.set('view cache', false);
|
||||
|
||||
app.use(favicon(dirname + '/public/favicon.ico'));
|
||||
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(compression());
|
||||
app.use(cookieParser());
|
||||
|
||||
app.use(require('./session')(config));
|
||||
var passport = require('./passport-config')(app, config);
|
||||
|
||||
app.use(express.static(path.join(dirname, 'public')));
|
||||
|
||||
app.use(require('./scrubbedUrl'));
|
||||
app.use(require('./logger'));
|
||||
if (process.env.WEBSITE_SKU) {
|
||||
app.use(require('./requireSecureAppService'));
|
||||
}
|
||||
app.use(require('./correlationId'));
|
||||
app.use(require('./locals'));
|
||||
|
||||
require('./passport-routes')(app, passport);
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var os = require('os');
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Set local variables that we want every view to share.
|
||||
// ----------------------------------------------------------------------------
|
||||
module.exports = function (req, res, next) {
|
||||
req.app.locals.correlationId = req.correlationId;
|
||||
req.app.locals.serverName = os.hostname();
|
||||
req.app.locals.appInsightsKey = req.app.settings.runtimeConfig.applicationInsights.instrumentationKey;
|
||||
|
||||
next();
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var logger = require('morgan');
|
||||
|
||||
logger.token('github', function getGitHub(req) {
|
||||
if (req.user && req.user.github && req.user.github.username) {
|
||||
return req.user.github.username;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
logger.token('correlationId', function getCorrelationId(req) {
|
||||
return req.correlationId ? req.correlationId : undefined;
|
||||
});
|
||||
|
||||
logger.token('scrubbedUrl', function getScrubbedUrl(req) {
|
||||
return req.scrubbedUrl || req.originalUrl || req.url;
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Use the customized ogger for Express requests.
|
||||
// ----------------------------------------------------------------------------
|
||||
module.exports = logger(':github :method :scrubbedUrl :status :response-time ms - :res[content-length] :correlationId');
|
|
@ -0,0 +1,84 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var passport = require('passport');
|
||||
var utils = require('../utils');
|
||||
|
||||
var GitHubStrategy = require('passport-github').Strategy;
|
||||
var OIDCStrategy = require('passport-azure-ad').OIDCStrategy;
|
||||
|
||||
module.exports = function (app, config) {
|
||||
// ----------------------------------------------------------------------------
|
||||
// GitHub Passport session setup.
|
||||
// ----------------------------------------------------------------------------
|
||||
// To support persistent login sessions, Passport needs to be able to
|
||||
// serialize users into and deserialize users out of the session. Typically,
|
||||
// this will be as simple as storing the user ID when serializing, and finding
|
||||
// the user by ID when deserializing. However, since this example does not
|
||||
// have a database of user records, the complete GitHub profile is serialized
|
||||
// and deserialized.
|
||||
passport.serializeUser(function(user, done) {
|
||||
done(null, user);
|
||||
});
|
||||
passport.deserializeUser(function(obj, done) {
|
||||
done(null, obj);
|
||||
});
|
||||
var gitHubTokenToSubset = function (accessToken, refreshToken, profile, done) {
|
||||
var subset = {
|
||||
github: {
|
||||
accessToken: accessToken,
|
||||
avatarUrl: profile._json && profile._json.avatar_url ? profile._json.avatar_url : undefined,
|
||||
displayName: profile.displayName,
|
||||
id: profile.id,
|
||||
profileUrl: profile.profileUrl,
|
||||
username: profile.username,
|
||||
}
|
||||
};
|
||||
return done(null, subset);
|
||||
};
|
||||
passport.use(new GitHubStrategy({
|
||||
clientID: config.github.clientId,
|
||||
clientSecret: config.github.clientSecret,
|
||||
callbackURL: config.github.callbackUrl,
|
||||
scope: ['user:email'],
|
||||
userAgent: 'passport-azure-oss-portal-for-github' // CONSIDER: User agent should be configured.
|
||||
}, gitHubTokenToSubset));
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Azure Active Directory Passport session setup.
|
||||
// ----------------------------------------------------------------------------
|
||||
var aadStrategy = new OIDCStrategy({
|
||||
callbackURL: config.activeDirectory.redirectUrl,
|
||||
realm: config.activeDirectory.tenantId,
|
||||
clientID: config.activeDirectory.clientId,
|
||||
clientSecret: config.activeDirectory.clientSecret,
|
||||
//oidcIssuer: config.creds.issuer,
|
||||
identityMetadata: 'https://login.microsoftonline.com/common/.well-known/openid-configuration',
|
||||
skipUserProfile: true,
|
||||
responseType: 'id_token code',
|
||||
responseMode: 'form_post',
|
||||
}, function (iss, sub, profile, accessToken, refreshToken, done) {
|
||||
done(null, profile);
|
||||
});
|
||||
passport.use('azure-active-directory', aadStrategy);
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Expanded OAuth-scope GitHub access for org membership writes.
|
||||
// ----------------------------------------------------------------------------
|
||||
var expandedGitHubScopeStrategy = new GitHubStrategy({
|
||||
clientID: config.github.clientId,
|
||||
clientSecret: config.github.clientSecret,
|
||||
callbackURL: config.github.callbackUrl + '/increased-scope',
|
||||
scope: ['user:email', 'write:org'],
|
||||
userAgent: 'passport-azure-oss-portal-for-github' // CONSIDER: User agent should be configured.
|
||||
}, gitHubTokenToSubset);
|
||||
|
||||
passport.use('expanded-github-scope', expandedGitHubScopeStrategy);
|
||||
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
return passport;
|
||||
};
|
|
@ -0,0 +1,106 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
module.exports = function configurePassport(app, passport) {
|
||||
// ----------------------------------------------------------------------------
|
||||
// passport integration with GitHub
|
||||
// ----------------------------------------------------------------------------
|
||||
app.get('/signin/github', function (req, res) {
|
||||
if (req.session && req.headers && req.headers.referer) {
|
||||
req.session.referer = req.headers.referer;
|
||||
}
|
||||
return res.redirect('/auth/github');
|
||||
});
|
||||
|
||||
app.get('/auth/github',
|
||||
passport.authenticate('github'),
|
||||
function (req, res){
|
||||
// The request will be redirected to GitHub for authentication, so this
|
||||
// function will not be called.
|
||||
});
|
||||
|
||||
app.get('/auth/github/callback',
|
||||
passport.authenticate('github', { failureRedirect: '/failure/github' }),
|
||||
function (req, res) {
|
||||
var url = '/';
|
||||
if (req.session && req.session.referer) {
|
||||
url = req.session.referer;
|
||||
delete req.session.referer;
|
||||
}
|
||||
res.redirect(url);
|
||||
});
|
||||
|
||||
app.get('/signout', function (req, res) {
|
||||
req.logout();
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Expanded GitHub auth scope routes
|
||||
// ----------------------------------------------------------------------------
|
||||
app.get('/signin/github/increased-scope', function (req, res){
|
||||
if (req.session && req.headers && req.headers.referer) {
|
||||
req.session.referer = req.headers.referer;
|
||||
}
|
||||
return res.redirect('/auth/github/increased-scope');
|
||||
});
|
||||
|
||||
app.get('/auth/github/increased-scope', passport.authorize('expanded-github-scope'));
|
||||
|
||||
app.get('/auth/github/callback/increased-scope',
|
||||
passport.authorize('expanded-github-scope'), function (req, res, next) {
|
||||
var account = req.account;
|
||||
var user = req.user;
|
||||
user.github.increasedScope = account;
|
||||
var url = '/';
|
||||
if (req.session && req.session.referer) {
|
||||
url = req.session.referer;
|
||||
delete req.session.referer;
|
||||
}
|
||||
res.redirect(url);
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// passport integration with Azure Active Directory
|
||||
// ----------------------------------------------------------------------------
|
||||
app.get('/auth/azure', passport.authorize('azure-active-directory'));
|
||||
|
||||
app.post('/auth/azure/callback',
|
||||
passport.authorize('azure-active-directory'), function (req, res, next) {
|
||||
var account = req.account;
|
||||
var username = account._json.upn;
|
||||
if (account !== null && username && account.displayName) {
|
||||
req.user.azure = {
|
||||
displayName: account.displayName,
|
||||
oid: account._json.oid,
|
||||
username: username,
|
||||
};
|
||||
var url = '/';
|
||||
if (req.session && req.session.referer) {
|
||||
url = req.session.referer;
|
||||
delete req.session.referer;
|
||||
}
|
||||
return res.redirect(url);
|
||||
} else {
|
||||
return next(new Error('Azure Active Directory authentication failed.'));
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/signin/azure', function(req, res){
|
||||
if (req.session && req.headers && req.headers.referer) {
|
||||
if (req.session.referer === undefined) {
|
||||
req.session.referer = req.headers.referer;
|
||||
}
|
||||
}
|
||||
return res.redirect('/auth/azure');
|
||||
});
|
||||
|
||||
app.get('/signout/azure', function(req, res){
|
||||
if (req.user && req.user.azure) {
|
||||
delete req.user.azure;
|
||||
}
|
||||
res.redirect('/');
|
||||
});
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// If this portal is deployed to Azure App Service, let's make sure that they
|
||||
// are connecting over SSL by validating the load balancer headers. If they are
|
||||
// not, redirect them. Keys off of WEBSITE_SKU env variable that is injected.
|
||||
// ----------------------------------------------------------------------------
|
||||
module.exports = function (req, res, next) {
|
||||
if (!req.headers['x-arr-ssl']) {
|
||||
return res.redirect('https://' + req.headers.host + req.originalUrl);
|
||||
} else {
|
||||
var arr = req.headers['x-arr-ssl'];
|
||||
var expectedHeaders = [
|
||||
'2048|128|C=US, S=Washington, L=Redmond, O=Microsoft Corporation, OU=Microsoft IT, CN=Microsoft IT SSL SHA2|CN=*.azurewebsites.net',
|
||||
'2048|256|C=US, S=Washington, L=Redmond, O=Microsoft Corporation, OU=Microsoft IT, CN=Microsoft IT SSL SHA2|CN=*.azurewebsites.net'
|
||||
];
|
||||
var isLegit = false;
|
||||
for (var i = 0; i < expectedHeaders.length; i++) {
|
||||
if (arr === expectedHeaders[i]) {
|
||||
isLegit = true;
|
||||
}
|
||||
}
|
||||
if (isLegit === false) {
|
||||
var err = new Error('The SSL connection may not be secured via Azure App Service. Please contact the site sponsors to investigate.');
|
||||
err.headers = req.headers;
|
||||
err.arrHeader = arr;
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
next();
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Scrub the incoming URL value(s) in the request, replacing tokens and other
|
||||
// secrets.
|
||||
// ----------------------------------------------------------------------------
|
||||
module.exports = function (req, res, next) {
|
||||
var url = req.originalUrl || req.url;
|
||||
var secretKeys = [
|
||||
'code',
|
||||
'token',
|
||||
];
|
||||
for (var i = 0; i < secretKeys.length; i++) {
|
||||
var key = secretKeys[i];
|
||||
var value = req.query[key];
|
||||
if (value !== undefined) {
|
||||
url = url.replace(key + '=' + value, key + '=*****');
|
||||
}
|
||||
}
|
||||
req.scrubbedUrl = url;
|
||||
next();
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var session = require('express-session');
|
||||
var RedisStore = require('connect-redis')(session);
|
||||
|
||||
module.exports = function (config) {
|
||||
var settings = {
|
||||
store: new RedisStore({
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
pass: config.redis.key,
|
||||
ttl: config.redis.ttl
|
||||
}),
|
||||
secret: config.express.sessionSalt,
|
||||
name: 'sid',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
maxAge: config.redis.ttl * 1000 /* milliseconds for maxAge, not seconds */
|
||||
}
|
||||
};
|
||||
return session(settings);
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
// This file as it stands is a stub.
|
||||
|
||||
var insertAuditLogEntry = function insertAuditEntry(details, callback) {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
|
@ -0,0 +1,581 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var async = require('async');
|
||||
var debug = require('debug')('azureossportal');
|
||||
var utils = require('../utils');
|
||||
var github = require('octonode');
|
||||
|
||||
var Org = require('./org');
|
||||
var Team = require('./team');
|
||||
var User = require('./user');
|
||||
var Repo = require('./repo');
|
||||
var RedisHelper = require('./redis');
|
||||
|
||||
function OpenSourceUserContext (applicationConfiguration, dataClient, user, redisInstance, callback) {
|
||||
var self = this;
|
||||
var modernUser;
|
||||
this.cache = {
|
||||
orgs: {},
|
||||
users: {},
|
||||
};
|
||||
this.modernUser = function () {
|
||||
return modernUser;
|
||||
};
|
||||
this.setting = function (name) {
|
||||
return applicationConfiguration[name];
|
||||
};
|
||||
this.dataClient = function () {
|
||||
return dataClient;
|
||||
};
|
||||
this.redisClient = function () {
|
||||
return redisInstance;
|
||||
};
|
||||
this.requestUser = function () {
|
||||
return user;
|
||||
};
|
||||
this.safeConfigurationTemp = safeSettings(applicationConfiguration);
|
||||
this.authenticated = {
|
||||
github: user && user.github && user.github.id,
|
||||
azure: user && user.azure && user.azure.username,
|
||||
};
|
||||
this.entities = {
|
||||
link: null,
|
||||
primaryMembership: null,
|
||||
};
|
||||
this.usernames = {
|
||||
github: user && user.github && user.github.username ? user.github.username : undefined,
|
||||
azure: user && user.azure && user.azure.username ? user.azure.username : undefined,
|
||||
};
|
||||
this.id = {
|
||||
github: user && user.github && user.github.id ? user.github.id.toString() : undefined,
|
||||
};
|
||||
if (this.id.github) {
|
||||
modernUser = new User(this, this.id.github);
|
||||
modernUser.login = this.usernames.github;
|
||||
}
|
||||
this.baseUrl = '/';
|
||||
this.redis = new RedisHelper(this, applicationConfiguration.redis.prefix);
|
||||
this.initializeBasics(function () {
|
||||
if (callback) {
|
||||
return callback(null, self);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Populate the user's OSS context object.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.initializeBasics = function (callback) {
|
||||
var userObject = this.modernUser();
|
||||
if (!userObject) {
|
||||
return callback(new Error("There's a logic bug in the user context object. We cannot continue."));
|
||||
}
|
||||
var self = this;
|
||||
userObject.getLink(function (error, link) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error, 'We were not able to retrieve information about any link for your user account at this time.'));
|
||||
}
|
||||
if (link) {
|
||||
self.entities.link = link;
|
||||
}
|
||||
callback(null, false);
|
||||
/*self.org().queryUserMembership(true, function (error, result) {
|
||||
// CONSIDER: This is part of the isAdministrator updates...
|
||||
if (result && result.state && result.role && result.role === 'admin') {
|
||||
self.entities.primaryMembership = result;
|
||||
}
|
||||
callback(null, false);
|
||||
});
|
||||
*/
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// SECURITY METHOD:
|
||||
// Determine whether the authenticated user is an Administrator of the org. At
|
||||
// this time there is a special "portal sudoers" team that is used. The GitHub
|
||||
// admin flag is not used [any longer] for performance reasons to reduce REST
|
||||
// calls to GitHub.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.isPortalAdministrator = function (callback) {
|
||||
/*
|
||||
var self = this;
|
||||
if (self.entities && self.entities.primaryMembership) {
|
||||
var pm = self.entities.primaryMembership;
|
||||
if (pm.role && pm.role === 'admin') {
|
||||
return callback(null, true);
|
||||
}
|
||||
}
|
||||
*/
|
||||
this.org().getPortalSudoersTeam().isMember(function (error, isMember) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error,
|
||||
'We had trouble querying GitHub for important team management ' +
|
||||
'information. Please try again later or report this issue.'));
|
||||
}
|
||||
callback(null, isMember === true);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Create a simple GitHub client. Should be audited, since using this library
|
||||
// directly may result in methods which are not cached, etc.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.createGenericGitHubClient = function () {
|
||||
var ownerToken = this.org().setting('ownerToken');
|
||||
if (!ownerToken) {
|
||||
throw new Error('No "ownerToken" set for the ' + this.org().name + ' organization.');
|
||||
}
|
||||
return github.client(ownerToken);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Make sure system links are loaded for a set of users.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.getLinksForUsers = function (list, callback) {
|
||||
var dc = this.dataClient();
|
||||
async.map(list, function (person, cb) {
|
||||
if (person && person.id) {
|
||||
cb(null, person.id);
|
||||
} else {
|
||||
cb(new Error('No ID known for this person instance.'));
|
||||
}
|
||||
}, function (error, map) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
// In large organizations, we will have trouble getting this much data back
|
||||
// all at once.
|
||||
var groups = [];
|
||||
var j = 0;
|
||||
var perGroup = 200;
|
||||
var group = [];
|
||||
for (var i = 0; i < map.length; i++) {
|
||||
if (j++ == perGroup) {
|
||||
groups.push(group);
|
||||
group = [];
|
||||
j = 0;
|
||||
}
|
||||
group.push(map[i]);
|
||||
}
|
||||
if (group.length > 0) {
|
||||
groups.push(group);
|
||||
group = [];
|
||||
}
|
||||
async.each(groups, function (userGroup, cb) {
|
||||
dc.getUserLinks(userGroup, function (error, links) {
|
||||
if (error) {
|
||||
// Specific to problems we've had with storage results...
|
||||
if (error.headers && error.headers.statusCode && error.headers.body) {
|
||||
var oldError = error;
|
||||
error = new Error('Storage returned an HTTP ' + oldError.headers.statusCode + '.');
|
||||
console.error.log(oldError.headers.body);
|
||||
error.innerError = oldError;
|
||||
}
|
||||
return cb(error);
|
||||
}
|
||||
// So inefficient and lazy:
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
list[i].trySetLinkInstance(links, true);
|
||||
}
|
||||
cb();
|
||||
});
|
||||
}, function (error) {
|
||||
callback(error ? error : null, error ? null : list);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Translate a list of IDs into developed objects and their system links.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.getUsersAndLinksFromIds = function (list, callback) {
|
||||
var self = this;
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var id = list[i];
|
||||
list[i] = self.user(id);
|
||||
}
|
||||
self.getLinksForUsers(list, callback);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Translate a hash of IDs to usernames into developed objects, system links
|
||||
// and details loaded. Hash key is username, ID is the initial hash value.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.getCompleteUsersFromUsernameIdHash = function (hash, callback) {
|
||||
var self = this;
|
||||
var users = {};
|
||||
var list = [];
|
||||
for (var key in hash) {
|
||||
var id = hash[key];
|
||||
var username = key;
|
||||
var user = self.user(id);
|
||||
user.login = username;
|
||||
users[username] = user;
|
||||
list.push(user);
|
||||
}
|
||||
async.parallel([
|
||||
function (cb) {
|
||||
self.getLinksForUsers(list, cb);
|
||||
},
|
||||
function (cb) {
|
||||
async.each(list, function (user, innerCb) {
|
||||
user.getDetailsByUsername(innerCb);
|
||||
}, function (error) {
|
||||
cb(error);
|
||||
});
|
||||
},
|
||||
], function (error) {
|
||||
callback(error, users);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve all organizations that the user is a member of, if any.
|
||||
// Caching: this set of calls can optionally turn off Redis caching, for use
|
||||
// during onboarding.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.getMyOrganizations = function (allowCaching, callback) {
|
||||
var self = this;
|
||||
if (typeof allowCaching == 'function') {
|
||||
callback = allowCaching;
|
||||
allowCaching = true;
|
||||
}
|
||||
var orgs = [];
|
||||
async.each(self.orgs(), function (org, callback) {
|
||||
org.queryUserMembership(allowCaching, function (error, result) {
|
||||
var state = false;
|
||||
if (result && result.state) {
|
||||
state = result.state;
|
||||
}
|
||||
// Not sure how I feel about updating values on the org directly...
|
||||
org.membershipStateTemporary = state;
|
||||
orgs.push(org);
|
||||
callback(error);
|
||||
});
|
||||
}, function (error) {
|
||||
callback(null, orgs);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve all of the teams -across all registered organizations. This is not
|
||||
// specific to the user. This will include secret teams.
|
||||
// Caching: the org.getTeams call has an internal cache at this time.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.getAllOrganizationsTeams = function (callback) {
|
||||
var self = this;
|
||||
async.concat(self.orgs(), function (org, cb) {
|
||||
org.getTeams(cb);
|
||||
}, function (error, teams) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
// CONSIDER: SORT: Do these results need to be sorted?
|
||||
callback(null, teams);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// This function uses heavy use of caching since it is an expensive set of
|
||||
// calls to make to the GitHub API when the cache misses: N API calls for N
|
||||
// teams in M organizations.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.getMyTeamMemberships = function (role, alternateUserId, callback) {
|
||||
var self = this;
|
||||
if (typeof alternateUserId == 'function') {
|
||||
callback = alternateUserId;
|
||||
alternateUserId = self.id.github;
|
||||
}
|
||||
this.getAllOrganizationsTeams(function (error, teams) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
var myTeams = [];
|
||||
async.each(teams, function (team, callback) {
|
||||
team.getMembersCached(role, function (error, members) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
for (var i = 0; i < members.length; i++) {
|
||||
var member = members[i];
|
||||
if (member.id == alternateUserId) {
|
||||
myTeams.push(team);
|
||||
break;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
callback(error, myTeams);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Designed for use by tooling, this returns the full set of administrators of
|
||||
// teams across all orgs. Designed to help setup communication with the people
|
||||
// using this portal for their daily engineering group work.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.getAllMaintainers = function (callback) {
|
||||
this.getAllOrganizationsTeams(function (error, teams) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
var users = {};
|
||||
async.each(teams, function (team, callback) {
|
||||
team.getMembersCached('maintainer', function (error, members) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
for (var i = 0; i < members.length; i++) {
|
||||
var member = members[i];
|
||||
if (users[member.id] === undefined) {
|
||||
users[member.id] = member;
|
||||
}
|
||||
// A dirty patch on top, just to save time now.
|
||||
if (users[member.id]._getAllMaintainersTeams === undefined) {
|
||||
users[member.id]._getAllMaintainersTeams = {};
|
||||
}
|
||||
users[member.id]._getAllMaintainersTeams[team.id] = team;
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
var asList = [];
|
||||
for (var key in users) {
|
||||
var user = users[key];
|
||||
asList.push(user);
|
||||
}
|
||||
async.each(asList, function (user, cb) {
|
||||
user.getLink(cb);
|
||||
}, function (error) {
|
||||
callback(error, asList);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve a set of team results.
|
||||
// ----------------------------------------------------------------------------
|
||||
// [_] CONSIDER: Cache/ Consider caching this sort of important return result...
|
||||
OpenSourceUserContext.prototype.getTeamSet = function (teamIds, inflate, callback) {
|
||||
var self = this;
|
||||
if (typeof inflate === 'function') {
|
||||
callback = inflate;
|
||||
inflate = false;
|
||||
}
|
||||
var teams = [];
|
||||
async.each(teamIds, function (teamId, cb) {
|
||||
self.getTeam(teamId, inflate, function (error, team) {
|
||||
if (!error) {
|
||||
teams.push(team);
|
||||
}
|
||||
cb(error);
|
||||
});
|
||||
}, function (error) {
|
||||
// CONSIDER: SORT: Do these results need to be sorted?
|
||||
callback(error, teams);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve a single team instance. This version hydrates the team's details
|
||||
// and also sets the organization instance.
|
||||
// ----------------------------------------------------------------------------
|
||||
// [_] CONSIDER: Cache/ Consider caching this sort of important return result...
|
||||
OpenSourceUserContext.prototype.getTeam = function (teamId, callback) {
|
||||
var self = this;
|
||||
var team = createBareTeam(self, teamId);
|
||||
team.getDetails(function (error) {
|
||||
if (error) {
|
||||
error = utils.wrapError(error, 'There was a problem retrieving the details for the team. The team may no longer exist.');
|
||||
}
|
||||
callback(error, error ? null : team);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Prepare a list of all organization names, lowercased, from the original
|
||||
// config instance.
|
||||
// ----------------------------------------------------------------------------
|
||||
function allOrgNamesLowercase(orgs) {
|
||||
var list = [];
|
||||
if (orgs && orgs.length) {
|
||||
for (var i = 0; i < orgs.length; i++) {
|
||||
var name = orgs[i].name;
|
||||
if (!name) {
|
||||
throw new Error('No organization name has been provided for one of the configured organizations.');
|
||||
}
|
||||
list.push(name.toLowerCase());
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve an array of all organizations registered for management with this
|
||||
// portal instance. Used for iterating through global operations. We'll need to
|
||||
// use smart caching to land this experience better than in the past, and to
|
||||
// preserve API use rates.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.orgs = function getAllOrgs() {
|
||||
var self = this;
|
||||
var allOrgNames = allOrgNamesLowercase(self.setting('organizations'));
|
||||
var orgs = [];
|
||||
for (var i = 0; i < allOrgNames.length; i++) {
|
||||
orgs.push(self.org(allOrgNames[i]));
|
||||
}
|
||||
return orgs;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve a user-scoped elevated organization object via a static
|
||||
// configuration lookup.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.org = function getOrg(orgNameAnycase) {
|
||||
if (orgNameAnycase === undefined || orgNameAnycase === '') {
|
||||
orgNameAnycase = this.setting('organizations')[0].name;
|
||||
}
|
||||
var name = orgNameAnycase.toLowerCase();
|
||||
if (this.cache.orgs[name]) {
|
||||
return this.cache.orgs[name];
|
||||
}
|
||||
var settings;
|
||||
var orgs = this.setting('organizations');
|
||||
for (var i = 0; i < orgs.length; i++) {
|
||||
if (orgs[i].name && orgs[i].name.toLowerCase() == name) {
|
||||
settings = orgs[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!settings) {
|
||||
throw new Error('The requested organization "' + orgNameAnycase + '" is not currently available for actions or is not configured for use at this time.');
|
||||
}
|
||||
var tr = this.setting('corporate').trainingResources;
|
||||
if (tr && tr['onboarding-complete']) {
|
||||
var tro = tr['onboarding-complete'];
|
||||
var trainingResources = {
|
||||
corporate: tro.all,
|
||||
github: tro.github,
|
||||
};
|
||||
if (tro[name]) {
|
||||
trainingResources.organization = tro[name];
|
||||
}
|
||||
settings.trainingResources = trainingResources;
|
||||
}
|
||||
this.cache.orgs[name] = new Org(this, settings.name, settings);
|
||||
return this.cache.orgs[name];
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve an object representing the user, by GitHub ID.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.user = function getUser(id, optionalGitHubInstance) {
|
||||
var self = this;
|
||||
if (typeof id != 'string') {
|
||||
id = id.toString();
|
||||
}
|
||||
if (self.cache.users[id]) {
|
||||
return self.cache.users[id];
|
||||
} else {
|
||||
self.cache.users[id] = new User(self, id, optionalGitHubInstance);
|
||||
return self.cache.users[id];
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Allows creating a team reference with just a team ID, no org instance.
|
||||
// ----------------------------------------------------------------------------
|
||||
function createBareTeam(oss, teamId) {
|
||||
var teamInstance = new Team(oss.org(), teamId, null);
|
||||
teamInstance.org = null;
|
||||
return teamInstance;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helper function for UI: Store in the user's session an alert message or
|
||||
// action to be shown in another successful render. Contexts come from Twitter
|
||||
// Bootstrap, i.e. 'success', 'info', 'warning', 'danger'.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.saveUserAlert = function (req, message, title, context, optionalLink, optionalCaption) {
|
||||
var alert = {
|
||||
message: message,
|
||||
title: title || 'FYI',
|
||||
context: context || 'success',
|
||||
optionalLink: optionalLink,
|
||||
optionalCaption: optionalCaption,
|
||||
};
|
||||
if (req.session) {
|
||||
if (req.session.alerts && req.session.alerts.length) {
|
||||
req.session.alerts.push(alert);
|
||||
} else {
|
||||
req.session.alerts = [
|
||||
alert,
|
||||
];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function safeSettings(config) {
|
||||
// CONSIDER: IMPLEMENT.
|
||||
return config;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helper function for UI: Render a view. By using our own rendering function,
|
||||
// we can make sure that events such as alert views are still actually shown,
|
||||
// even through redirect sequences.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.render = function (req, res, view, title, optionalObject) {
|
||||
if (typeof title == 'object') {
|
||||
optionalObject = title;
|
||||
title = '';
|
||||
debug('context::render: the provided title was actually an object');
|
||||
}
|
||||
var breadcrumbs = req.breadcrumbs;
|
||||
if (breadcrumbs && breadcrumbs.length && breadcrumbs.length > 0) {
|
||||
breadcrumbs[breadcrumbs.length - 1].isLast = true;
|
||||
}
|
||||
var obj = {
|
||||
title: title,
|
||||
config: this.safeConfigurationTemp,
|
||||
serviceBanner: this.setting('serviceBanner'),
|
||||
user: this.requestUser(),
|
||||
ossLink: this.entities.link,
|
||||
showBreadcrumbs: true,
|
||||
breadcrumbs: breadcrumbs,
|
||||
sudoMode: req.sudoMode,
|
||||
};
|
||||
if (optionalObject) {
|
||||
utils.merge(obj, optionalObject);
|
||||
}
|
||||
if (req.session && req.session.alerts && req.session.alerts.length && req.session.alerts.length > 0) {
|
||||
var alerts = [];
|
||||
utils.merge(alerts, req.session.alerts);
|
||||
req.session.alerts = [];
|
||||
for (var i = 0; i < alerts.length; i++) {
|
||||
if (typeof alerts[i] == 'object') {
|
||||
alerts[i].number = i + 1;
|
||||
}
|
||||
}
|
||||
obj.alerts = alerts;
|
||||
}
|
||||
res.render(view, obj);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Cheap breadcrumbs on a request object as it goes through our routes. Does
|
||||
// not actually store anything in the OSS instance at this time.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUserContext.prototype.addBreadcrumb = function (req, breadcrumbTitle, optionalBreadcrumbLink) {
|
||||
utils.addBreadcrumb(req, breadcrumbTitle, optionalBreadcrumbLink);
|
||||
};
|
||||
|
||||
module.exports = OpenSourceUserContext;
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
// CONSIDER: Cleanup issue.js.
|
||||
|
||||
function OpenSourceIssue (repoInstance, issueNumber, optionalInitialData) {
|
||||
this.repo = repoInstance;
|
||||
if (!repoInstance.full_name) {
|
||||
throw new Error('No full_name set for this instance.');
|
||||
}
|
||||
this.oss = repoInstance.oss;
|
||||
this.number = issueNumber;
|
||||
if (optionalInitialData) {
|
||||
throw new Error('optionalInitialData is not yet supported for the OpenSourceIssue type.');
|
||||
}
|
||||
}
|
||||
|
||||
OpenSourceIssue.prototype.createComment = function (body, callback) {
|
||||
this.oss.createGenericGitHubClient().issue(this.repo.full_name, this.number).createComment({
|
||||
body: body
|
||||
}, callback);
|
||||
};
|
||||
|
||||
OpenSourceIssue.prototype.update = function (patch, callback) {
|
||||
this.oss.createGenericGitHubClient().issue(this.repo.full_name, this.number).update(patch, callback);
|
||||
};
|
||||
|
||||
OpenSourceIssue.prototype.close = function (callback) {
|
||||
this.oss.createGenericGitHubClient().issue(this.repo.full_name, this.number).update({
|
||||
state: 'closed',
|
||||
}, callback);
|
||||
};
|
||||
|
||||
module.exports = OpenSourceIssue;
|
|
@ -0,0 +1,709 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var async = require('async');
|
||||
var github = require('octonode');
|
||||
var debug = require('debug')('azureossportal');
|
||||
var utils = require('../utils');
|
||||
var Team = require('./team');
|
||||
var Repo = require('./repo');
|
||||
|
||||
function OpenSourceOrganization (ossInstance, name, settings) {
|
||||
var self = this;
|
||||
self.name = name;
|
||||
// CONSIDER: Do not expose.
|
||||
self.inner = {
|
||||
settings: settings,
|
||||
teams: {},
|
||||
repos: {},
|
||||
};
|
||||
self.oss = ossInstance;
|
||||
self.baseUrl = self.oss.baseUrl + name + '/';
|
||||
self.setting = function (name) {
|
||||
var value = self.inner.settings[name];
|
||||
if (value === undefined) {
|
||||
debug('setting ' + name + ' is undefined!');
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Create a GitHub 'octonode' client using our standard owner elevation token
|
||||
// or an alternate token.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.createGenericGitHubClient = function createGitHubClient(alternateToken) {
|
||||
var ownerToken = this.inner.settings.ownerToken;
|
||||
if (!ownerToken) {
|
||||
throw new Error('No "ownerToken" available for the ' + this.name + ' organization.');
|
||||
}
|
||||
return github.client(alternateToken || ownerToken);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// With the GitHub OAuth scope of 'write:org', we can accept the invitation for
|
||||
// the user on their behalf, improving the onboarding workflow from our earlier
|
||||
// implementation with the invitation dance and hop.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.acceptOrganizationInvitation = function acceptInvite(userToken, callback) {
|
||||
if (!userToken) {
|
||||
return callback(new Error('No GitHub token available for the user operation.'));
|
||||
}
|
||||
this.createGenericGitHubClient(userToken).me().updateMembership(this.name, 'active', callback);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Special Team: "Everyone" team used for handling invites and 2FA checks.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getAllMembersTeam = function (throwIfMissing) {
|
||||
return getSpecialTeam(this, 'teamAllMembers', 'all members', throwIfMissing);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Special Team: "Repo Approvers" for the repo create workflow.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getRepoApproversTeam = function (throwIfMissing) {
|
||||
return getSpecialTeam(this, 'teamRepoApprovers', 'repo create approvers', throwIfMissing);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Get the highlighted teams for the org, if any.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getHighlightedTeams = function () {
|
||||
var highlightedTeams = this.inner.settings.highlightedTeams;
|
||||
var teams = [];
|
||||
if (utils.isArray(highlightedTeams)) {
|
||||
for (var i = 0; i < highlightedTeams.length; i++) {
|
||||
var team = this.team(highlightedTeams[i].id);
|
||||
teams.push(team);
|
||||
}
|
||||
}
|
||||
return teams;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Special Team: "All Repos" which receives access to all repos in the org.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getSecurityTeam = function (throwIfMissing) {
|
||||
return getSpecialTeam(this, 'teamAllRepos', 'all repos access team', throwIfMissing);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Special Team: "All Repo Write" which gives write access to all repos for
|
||||
// very specific engineering system use cases.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getAllRepoWriteTeam = function (throwIfMissing) {
|
||||
return getSpecialTeam(this, 'teamAllRepoWriteId', 'all repo write team', throwIfMissing);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve a user-scoped team object.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.team = function getTeam(id, optionalInitialData) {
|
||||
var self = this;
|
||||
if (typeof id != 'string') {
|
||||
id = id.toString();
|
||||
}
|
||||
if (self.inner.teams[id]) {
|
||||
var team = self.inner.teams[id];
|
||||
if (team._detailsLoaded === false && optionalInitialData) {
|
||||
team.setDetails(optionalInitialData);
|
||||
}
|
||||
return team;
|
||||
} else {
|
||||
self.inner.teams[id] = new Team(self, id, optionalInitialData);
|
||||
return self.inner.teams[id];
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve a user-scoped repo object.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.repo = function getRepo(repoName, optionalInitialData) {
|
||||
var self = this;
|
||||
var normalized = repoName.toLowerCase();
|
||||
if (self.inner.repos[normalized]) {
|
||||
return self.inner.repos[normalized];
|
||||
} else {
|
||||
self.inner.repos[normalized] = new Repo(self, repoName, optionalInitialData);
|
||||
return self.inner.repos[normalized];
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Get a repository client for the notifications repo used in the workflow.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getWorkflowRepository = function () {
|
||||
var repoName = this.inner.settings.notificationRepo;
|
||||
if (!repoName) {
|
||||
throw new Error('No workflow/notification repository is defined for the organization.');
|
||||
}
|
||||
return this.repo(repoName);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve a team object by name. To be much more efficient, this should
|
||||
// actually live in a global memory cache (vs per-user context like the other
|
||||
// OSS instances). But it works for now.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.teamFromName = function getTeamName(teamName, callback) {
|
||||
var self = this;
|
||||
this.getTeams(true /* allow caching */, function (error, teams) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
for (var i = 0; i < teams.length; i++) {
|
||||
var name = teams[i].name;
|
||||
var slug = teams[i].slug;
|
||||
if (name && name.toLowerCase && name.toLowerCase() == teamName.toLowerCase()) {
|
||||
var redirectError = null;
|
||||
if (name.toLowerCase() != slug.toLowerCase()) {
|
||||
redirectError = new Error();
|
||||
redirectError.status = 401;
|
||||
redirectError.slug = slug;
|
||||
}
|
||||
return callback(redirectError, teams[i]);
|
||||
}
|
||||
if (slug && slug.toLowerCase && slug.toLowerCase() == teamName.toLowerCase()) {
|
||||
return callback(null, teams[i]);
|
||||
}
|
||||
}
|
||||
// Make a secondary request without caching, to be sure... it may have just
|
||||
// been created, for example.
|
||||
self.getTeams(false, function (error, teams) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
for (var i = 0; i < teams.length; i++) {
|
||||
var name = teams[i].name;
|
||||
if (name && name.toLowerCase && name.toLowerCase() == teamName) {
|
||||
return callback(null, teams[i]);
|
||||
}
|
||||
}
|
||||
return callback(null, null);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// SECURITY METHOD:
|
||||
// Is the user in this context authorized as a sudoer of this organization?
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.isUserSudoer = function (callback) {
|
||||
this.getSudoersTeam().isMember(function (error, isMember) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error,
|
||||
'We had trouble querying GitHub for important team management ' +
|
||||
'information. Please try again later or report this issue.'));
|
||||
}
|
||||
callback(null, isMember === true);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Special Team: Sudoers for this specific organization. The members
|
||||
// of this team have semi-sudoers ability - the ability to maintain their org
|
||||
// as needed. It is important to notice that the organization that the sudoers
|
||||
// are in may actually be the primary org and not the leaf node org.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getSudoersTeam = function () {
|
||||
var self = this;
|
||||
var config = self.inner.settings;
|
||||
if (config && config.teamSudoers) {
|
||||
return self.team(config.teamSudoers);
|
||||
} else {
|
||||
throw new Error('Configuration for the sudoers team is missing.');
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Special Team: Portal sudoers. This only applies to the first org.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getPortalSudoersTeam = function () {
|
||||
var self = this;
|
||||
var config = self.inner.settings;
|
||||
if (config && config.teamPortalSudoers) {
|
||||
return self.team(config.teamPortalSudoers);
|
||||
} else {
|
||||
throw new Error('Configuration for the portal sudoers team is missing.');
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Check for public membership
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.queryUserPublicMembership = function getSingleUserMembership(callback) {
|
||||
var self = this;
|
||||
var ghorg = self.createGenericGitHubClient().org(self.name);
|
||||
ghorg.publicMember(self.oss.usernames.github, function (error, result) {
|
||||
return callback(null, result === true);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Make membership public for the authenticated user.
|
||||
// Requires an expanded GitHub API scope (write:org).
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.setPublicMembership = function goPublic(userToken, callback) {
|
||||
var ghorg = this.createGenericGitHubClient(userToken).org(this.name);
|
||||
ghorg.publicizeMembership(this.oss.usernames.github, callback);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Make membership private for the authenticated user.
|
||||
// Requires an expanded GitHub API scope (write:org).
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.setPrivateMembership = function goPrivate(userToken, callback) {
|
||||
var ghorg = this.createGenericGitHubClient(userToken).org(this.name);
|
||||
ghorg.concealMembership(this.oss.usernames.github, callback);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Create a repository on GitHub within this org.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.createRepository = function createRepo(name, properties, callback) {
|
||||
if (typeof properties == 'function') {
|
||||
callback = properties;
|
||||
properties = {};
|
||||
}
|
||||
var ghorg = this.createGenericGitHubClient().org(this.name);
|
||||
var repoProperties = {
|
||||
name: name,
|
||||
};
|
||||
utils.merge(repoProperties, properties);
|
||||
ghorg.repo(repoProperties, callback);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Check for membership (private or public). Use Redis for performance reasons
|
||||
// and fallback to a live API query for pending/negative results.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.queryUserMembership = function getSingleUserGeneralMembership(allowRedis, callback) {
|
||||
var self = this;
|
||||
if (typeof allowRedis == 'function') {
|
||||
callback = allowRedis;
|
||||
allowRedis = true;
|
||||
}
|
||||
if (allowRedis === true) {
|
||||
return self.queryUserMembershipCached(callback);
|
||||
}
|
||||
self.createGenericGitHubClient().org(self.name).membership(self.oss.usernames.github, function (error, result) {
|
||||
if (!(result && result.state && (result.state == 'active' || result.state == 'pending'))) {
|
||||
result = false;
|
||||
}
|
||||
var redisKey = 'user#' + self.oss.id.github + ':org#' + self.name + ':membership';
|
||||
self.oss.redis.setObjectWithExpire(redisKey, result, 60 * 48 /* 2 days */, function () {
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Check for membership (private or public) for any GitHub username. Does not
|
||||
// cache.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.queryAnyUserMembership = function (username, callback) {
|
||||
var self = this;
|
||||
self.createGenericGitHubClient().org(self.name).membership(username, function (error, result) {
|
||||
if (!(result && result.state && (result.state == 'active' || result.state == 'pending'))) {
|
||||
result = false;
|
||||
}
|
||||
callback(null, result);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Clears the cached state for a user's organization membership value.
|
||||
// ----------------------------------------------------------------------------
|
||||
function removeCachedUserMembership(self, callback) {
|
||||
var redisKey = 'user#' + self.oss.id.github + ':org#' + self.name + ':membership';
|
||||
self.oss.redis.delete(redisKey, function () {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Check for membership (private or public). Use Redis for performance reasons
|
||||
// (the "active" example) and always fallback to a live API query when needed.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.queryUserMembershipCached = function getSingleUserGeneralMembershipCached(callback) {
|
||||
var self = this;
|
||||
var redisKey = 'user#' + self.oss.id.github + ':org#' + self.name + ':membership';
|
||||
self.oss.redis.getObject(redisKey, function (error, data) {
|
||||
if (!error && data && data.state && data.state == 'active') {
|
||||
return callback(null, data);
|
||||
}
|
||||
self.createGenericGitHubClient().org(self.name).membership(self.oss.usernames.github, function (error, result) {
|
||||
if (error) {
|
||||
error = null;
|
||||
result = false;
|
||||
}
|
||||
self.oss.redis.setObjectWithExpire(redisKey, result, 60 * 48 /* 2 days */, function () {
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Remove the user from the organization.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.removeUserMembership = function dropUser(optionalUsername, callback) {
|
||||
var self = this;
|
||||
if (typeof optionalUsername == 'function') {
|
||||
callback = optionalUsername;
|
||||
optionalUsername = self.oss.usernames.github;
|
||||
}
|
||||
self.createGenericGitHubClient().org(self.name).removeMember(optionalUsername, function (error, result) {
|
||||
removeCachedUserMembership(self, function () {
|
||||
callback(error, result);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve the list of all teams in the organization. This is not specific to
|
||||
// the user but instead a general query across this org.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getTeams = function getBasicTeamList(allowRedis, callback) {
|
||||
var self = this;
|
||||
if (typeof allowRedis == 'function') {
|
||||
callback = allowRedis;
|
||||
allowRedis = true;
|
||||
}
|
||||
var instancesFromJson = function (teamInstances) {
|
||||
async.map(teamInstances, function (teamInstance, cb) {
|
||||
cb(null, self.team(teamInstance.id, teamInstance));
|
||||
}, callback);
|
||||
};
|
||||
var redisKey = 'org#' + self.name + ':teams';
|
||||
self.oss.redis.getObject(redisKey, function (error, data) {
|
||||
if (!error && data && allowRedis === true) {
|
||||
return instancesFromJson(data);
|
||||
}
|
||||
var ghorg = self.createGenericGitHubClient().org(self.name);
|
||||
utils.retrieveAllPages(ghorg.teams.bind(ghorg), function (error, teamInstances) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
self.oss.redis.setObjectWithExpire(redisKey, teamInstances, utils.randomInteger(20, 90), function () {
|
||||
instancesFromJson(teamInstances);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Clear the organization's team list cache.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.clearTeamsCache = function (callback) {
|
||||
var redisKey = 'org#' + this.name + ':teams';
|
||||
this.oss.redis.delete(redisKey, function () {
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Gets all source repos for the organization.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getRepos = function getRepos(allowRedis, callback) {
|
||||
var self = this;
|
||||
if (typeof allowRedis == 'function') {
|
||||
callback = allowRedis;
|
||||
allowRedis = true;
|
||||
}
|
||||
var instancesFromJson = function (repos) {
|
||||
async.map(repos, function (repo, cb) {
|
||||
cb(null, self.repo(repo.name, repo));
|
||||
}, callback);
|
||||
};
|
||||
var redisKey = 'org#' + self.name + ':repos';
|
||||
self.oss.redis.getObject(redisKey, function (error, data) {
|
||||
if (!error && data && allowRedis === true) {
|
||||
return instancesFromJson(data);
|
||||
}
|
||||
var ghorg = self.createGenericGitHubClient().org(self.name);
|
||||
utils.retrieveAllPages(ghorg.repos.bind(ghorg), {
|
||||
'type': 'sources',
|
||||
}, function (error, repos) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
self.oss.redis.setObjectWithExpire(redisKey, repos, utils.randomInteger(30, 60 * 12), function () {
|
||||
instancesFromJson(repos);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Gets a list of team memberships for the authenticated user. This is a slower
|
||||
// implementation than the GitHub API provides, since that requires additional
|
||||
// authenticated scope, which our users have had negative feedback about
|
||||
// requiring. Instead, this uses an org-authorized token vs the user's.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getMyTeamMemberships = function (role, alternateUserId, callback) {
|
||||
var self = this;
|
||||
if (typeof alternateUserId == 'function') {
|
||||
callback = alternateUserId;
|
||||
alternateUserId = self.oss.id.github;
|
||||
}
|
||||
self.getTeams(function (error, teams) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
var myTeams = [];
|
||||
async.each(teams, function (team, callback) {
|
||||
team.getMembersCached(role, function (error, members) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
for (var i = 0; i < members.length; i++) {
|
||||
var member = members[i];
|
||||
if (member.id == alternateUserId) {
|
||||
myTeams.push(team);
|
||||
break;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
callback(error, myTeams);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Builds a hash mapping organization member's GitHub user IDs to a cached
|
||||
// member object. This version actually walks all of the teams, which is a
|
||||
// super CPU-intensive way to do this, but it makes some use of Redis. Need
|
||||
// to fix that someday and cache the whole thing probably.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getAllMembersById = function (callback) {
|
||||
var self = this;
|
||||
var memberHash = {};
|
||||
self.getTeams(function (error, teams) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
async.each(teams, function (team, callback) {
|
||||
team.getMembersCached('all', function (error, members) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
for (var i = 0; i < members.length; i++) {
|
||||
var member = members[i];
|
||||
if (memberHash[member.id] === undefined) {
|
||||
memberHash[member.id] = member;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
callback(error, memberHash);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve the list of all accounts in the org that do not have multi-factor
|
||||
// (modern security) auth turned on. Uses the GitHub API. This version uses a
|
||||
// cache to speed up the use of the site.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getAuditListCached = function getAuditListCached(callback) {
|
||||
var self = this;
|
||||
var redisKey = 'org#' + self.name + ':2fa-disabled';
|
||||
var ghorg = this.createGenericGitHubClient().org(this.name);
|
||||
self.oss.redis.getObject(redisKey, function (error, data) {
|
||||
if (!error && data) {
|
||||
return mapUsernameToId(data, callback);
|
||||
}
|
||||
utils.retrieveAllPages(ghorg.members.bind(ghorg), { filter: '2fa_disabled' }, function (error, people) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
self.oss.redis.setObjectWithExpire(redisKey, people, 60 * 48 /* 2 days */, function () {
|
||||
mapUsernameToId(people, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve a hash, by username, of all admins for the organization.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getAdministratorsHashCached = function getAdminsCached(callback) {
|
||||
var self = this;
|
||||
var redisKey = 'org#' + self.name + ':admins';
|
||||
var ghorg = this.createGenericGitHubClient().org(this.name);
|
||||
self.oss.redis.getObject(redisKey, function (error, data) {
|
||||
if (!error && data) {
|
||||
return mapUsernameToId(data, callback);
|
||||
}
|
||||
utils.retrieveAllPages(ghorg.members.bind(ghorg), { role: 'admin' }, function (error, people) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
self.oss.redis.setObjectWithExpire(redisKey, people, 60 * 48 /* 2 days */, function () {
|
||||
mapUsernameToId(people, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Check whether this user has multi-factor authentication turned on. Returns
|
||||
// true for a user in good standing.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.queryUserMultifactorStateOk = function getSingleUserMfaState(callback) {
|
||||
var self = this;
|
||||
self.getAuditList(function (error, list) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error, 'A problem occurred while trying to query important information about the org.'));
|
||||
}
|
||||
var twoFactorOff = list[self.oss.usernames.github.toLowerCase()] !== undefined;
|
||||
callback(null, twoFactorOff === false);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Check whether this user has multi-factor authentication turned on. Returns
|
||||
// true for a user in good standing. Uses the cache initially. If the cache
|
||||
// result implies that this user may not be in compliance, we reach out with a
|
||||
// real GitHub API request, resetting the cache and writing the results. This
|
||||
// was the user only receives false in the case of an API failure or actually
|
||||
// not having multifactor authentication turned on.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.queryUserMultifactorStateOkCached = function getSingleUserMfaStateCached(callback) {
|
||||
var self = this;
|
||||
self.getAuditListCached(function (error, list) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error, 'A problem occurred while trying to query important information about the org.'));
|
||||
}
|
||||
var twoFactorOff = list[self.oss.usernames.github.toLowerCase()] !== undefined;
|
||||
if (twoFactorOff === false) {
|
||||
return callback(null, true);
|
||||
}
|
||||
// Go to the live version of the app...
|
||||
self.getAuditList(function (error, list) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error, 'A problem occurred while trying to read the current authentication state for your account. Please check that you have turned multifactor authentication on for your GitHub account - thanks.'));
|
||||
}
|
||||
var twoFactorOff = list[self.oss.usernames.github.toLowerCase()] !== undefined;
|
||||
callback(null, twoFactorOff === false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve the list of all accounts in the org that do not have multi-factor
|
||||
// (modern security) auth turned on. Uses the GitHub API.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getAuditList = function getAuditList(callback) {
|
||||
var self = this;
|
||||
var ghorg = this.createGenericGitHubClient().org(this.name);
|
||||
utils.retrieveAllPages(ghorg.members.bind(ghorg), { filter: '2fa_disabled' }, function (error, people) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
// Cache the result, updating the org-wide view...
|
||||
var redisKey = 'org#' + self.name + ':2fa-disabled';
|
||||
self.oss.redis.setObjectWithExpire(redisKey, people, 60 * 48 /* 2 days */, function () {
|
||||
mapUsernameToId(people, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Clear the cached MFA list for this organization.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.clearAuditList = function clearAuditList(callback) {
|
||||
var self = this;
|
||||
var redisKey = 'org#' + self.name + ':2fa-disabled';
|
||||
self.oss.redis.delete(redisKey, function () {
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Get the cached high-level information from GitHub for this organization.
|
||||
// Unlike the team and user objects, these properties are simply returned to
|
||||
// the caller and not merged into the type and its values.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getDetails = function getOrgDetails(allowRedis, callback) {
|
||||
var self = this;
|
||||
if (typeof allowRedis == 'function') {
|
||||
callback = allowRedis;
|
||||
allowRedis = true;
|
||||
}
|
||||
var redisKey = 'org#' + self.name + ':details';
|
||||
self.oss.redis.getObject(redisKey, function (error, data) {
|
||||
if (!error && data && allowRedis === true) {
|
||||
return callback(null, data);
|
||||
}
|
||||
var ghorg = self.createGenericGitHubClient().org(self.name);
|
||||
ghorg.info(function (error, info) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error, 'The GitHub API had trouble returning information about the organization ' + self.name));
|
||||
}
|
||||
self.oss.redis.setObjectWithExpire(redisKey, info, utils.randomInteger(60 * 24, 60 * 24 * 2), function () {
|
||||
callback(null, info);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Gets the organization's psuedo-user account details from GitHub.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganization.prototype.getOrganizationUserProfile = function getOrganizationUserProfile(callback) {
|
||||
var self = this;
|
||||
this.getDetails(function (error, details) {
|
||||
if (error || !details) {
|
||||
return callback(utils.wrapError(error, 'We had trouble retrieving the profile of the ' + self.name + ' organization from GitHub.'));
|
||||
}
|
||||
var user = self.oss.user(details.id, details);
|
||||
callback(null, user);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Private: Project a team members list to a dictionary of username:id.
|
||||
// ----------------------------------------------------------------------------
|
||||
function mapUsernameToId(people, callback) {
|
||||
var projected = {};
|
||||
async.each(people, function (person, cb) {
|
||||
if (person.id && person.login && person.login.toLowerCase) {
|
||||
projected[person.login.toLowerCase()] = person.id;
|
||||
}
|
||||
cb();
|
||||
}, function (error) {
|
||||
callback(error, error ? undefined : projected);
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Private: get a special team instance
|
||||
// ----------------------------------------------------------------------------
|
||||
function getSpecialTeam(org, configName, prettyName, throwIfMissing) {
|
||||
if (throwIfMissing === undefined) {
|
||||
throwIfMissing = true;
|
||||
}
|
||||
var mySettings = org.inner.settings;
|
||||
if (mySettings[configName]) {
|
||||
return org.team(mySettings[configName]);
|
||||
} else {
|
||||
var message = 'Configuration is missing. The "' + prettyName + '" team is not defined.';
|
||||
if (throwIfMissing === true) {
|
||||
throw new Error(message);
|
||||
} else {
|
||||
debug(message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OpenSourceOrganization;
|
|
@ -0,0 +1,99 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var async = require('async');
|
||||
var debug = require('debug')('oss-redis');
|
||||
var utils = require('../utils');
|
||||
|
||||
function RedisHelper (ossInstance, prefix) {
|
||||
this.oss = ossInstance;
|
||||
this.redis = ossInstance.redisClient();
|
||||
this.prefix = prefix ? prefix + ',' : '';
|
||||
}
|
||||
|
||||
function objectFromJson(json, callback) {
|
||||
var error = null;
|
||||
var object = null;
|
||||
try {
|
||||
if (json) {
|
||||
object = JSON.parse(json);
|
||||
}
|
||||
} catch (ex) {
|
||||
error = ex;
|
||||
object = null;
|
||||
}
|
||||
callback(error, object);
|
||||
}
|
||||
|
||||
function objectToJson(object, callback) {
|
||||
var error = null;
|
||||
var json = null;
|
||||
try {
|
||||
json = JSON.stringify(object);
|
||||
} catch (ex) {
|
||||
error = ex;
|
||||
}
|
||||
callback(error, json);
|
||||
}
|
||||
|
||||
RedisHelper.prototype.get = function (key, callback) {
|
||||
var k = this.prefix + key;
|
||||
// debug('GET ' + k);
|
||||
this.redis.get(k, callback);
|
||||
};
|
||||
|
||||
RedisHelper.prototype.set = function (key, value, callback) {
|
||||
var k = this.prefix + key;
|
||||
debug('SET ' + k);
|
||||
this.redis.set(k, value, callback);
|
||||
};
|
||||
|
||||
|
||||
RedisHelper.prototype.delete = function (key, callback) {
|
||||
var k = this.prefix + key;
|
||||
debug('DEL ' + k);
|
||||
this.redis.del(k, callback);
|
||||
};
|
||||
|
||||
RedisHelper.prototype.setWithExpire = function (key, value, minutesToExpire, callback) {
|
||||
var k = this.prefix + key;
|
||||
debug('SET ' + k + ' EX ' + minutesToExpire + 'm');
|
||||
this.redis.set(k, value, 'EX', minutesToExpire * 60, callback);
|
||||
};
|
||||
|
||||
// Helper versions for object/json conversions
|
||||
|
||||
RedisHelper.prototype.getObject = function (key, callback) {
|
||||
this.get(key, function (error, json) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
objectFromJson(json, callback);
|
||||
});
|
||||
};
|
||||
|
||||
RedisHelper.prototype.setObject = function (key, value, callback) {
|
||||
var self = this;
|
||||
objectToJson(value, function (error, json) {
|
||||
if (!error) {
|
||||
self.set(key, json, callback);
|
||||
} else {
|
||||
callback(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
RedisHelper.prototype.setObjectWithExpire = function (key, value, minutesToExpire, callback) {
|
||||
var self = this;
|
||||
objectToJson(value, function (error, json) {
|
||||
if (!error) {
|
||||
self.setWithExpire(key, json, minutesToExpire, callback);
|
||||
} else {
|
||||
callback(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = RedisHelper;
|
|
@ -0,0 +1,314 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var async = require('async');
|
||||
var github = require('octonode');
|
||||
var debug = require('debug')('azureossportal');
|
||||
var utils = require('../utils');
|
||||
var Issue = require('./issue');
|
||||
|
||||
function OpenSourceRepo (orgInstance, repoName, optionalGitHubInstance) {
|
||||
if (!orgInstance) {
|
||||
throw new Error('orgInstance is not defined.');
|
||||
}
|
||||
this.org = orgInstance;
|
||||
this.oss = this.org.oss;
|
||||
var i = repoName.indexOf('/');
|
||||
if (i >= 0) {
|
||||
this.full_name = repoName;
|
||||
var orgName = repoName.substring(0, i);
|
||||
repoName = repoName.substring(i + 1);
|
||||
if (orgName.toLowerCase() !== orgInstance.name.toLowerCase()) {
|
||||
debug('WARNING: The org name does not match: (' + orgName + ', ' + orgInstance.name + ')');
|
||||
}
|
||||
} else {
|
||||
this.full_name = orgInstance.name + '/' + repoName;
|
||||
}
|
||||
this.name = repoName;
|
||||
this.inner = {
|
||||
issues: {}
|
||||
};
|
||||
this.otherFields = {};
|
||||
this._detailsLoaded = false;
|
||||
if (optionalGitHubInstance) {
|
||||
setDetails(this, optionalGitHubInstance);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Properties of interest in the standard GitHub response for a user
|
||||
// ----------------------------------------------------------------------------
|
||||
var detailsToCopy = [
|
||||
'id',
|
||||
'name',
|
||||
'full_name',
|
||||
'private',
|
||||
'html_url',
|
||||
'description',
|
||||
'fork',
|
||||
'url',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'pushed_at',
|
||||
'git_url',
|
||||
'ssh_url',
|
||||
'clone_url',
|
||||
'homepage',
|
||||
'size',
|
||||
'stargazers_count',
|
||||
'watchers_count',
|
||||
'language',
|
||||
'has_issues',
|
||||
'has_downloads',
|
||||
'has_wiki',
|
||||
'has_pages',
|
||||
'forks_count',
|
||||
'open_issues_count',
|
||||
'forks',
|
||||
'open_issues',
|
||||
'watchers',
|
||||
'default_branch',
|
||||
'permissions',
|
||||
];
|
||||
var detailsToSkip = [
|
||||
'owner',
|
||||
'forks_url',
|
||||
'keys_url',
|
||||
'collaborators_url',
|
||||
'teams_url',
|
||||
'hooks_url',
|
||||
'issue_events_url',
|
||||
'events_url',
|
||||
'assignees_url',
|
||||
'branches_url',
|
||||
'tags_url',
|
||||
'blobs_url',
|
||||
'git_tags_url',
|
||||
'git_refs_url',
|
||||
'trees_url',
|
||||
'statuses_url',
|
||||
'languages_url',
|
||||
'stargazers_url',
|
||||
'contributors_url',
|
||||
'subscribers_url',
|
||||
'subscription_url',
|
||||
'commits_url',
|
||||
'git_commits_url',
|
||||
'comments_url',
|
||||
'issue_comment_url',
|
||||
'contents_url',
|
||||
'compare_url',
|
||||
'merges_url',
|
||||
'archive_url',
|
||||
'downloads_url',
|
||||
'issues_url',
|
||||
'pulls_url',
|
||||
'milestones_url',
|
||||
'notifications_url',
|
||||
'labels_url',
|
||||
'releases_url',
|
||||
'svn_url',
|
||||
'mirror_url',
|
||||
];
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Creates a GitHub API client for this repo.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceRepo.prototype.createGitHubRepoClient = function () {
|
||||
var client = this.org.createGenericGitHubClient();
|
||||
debug('creating repo client for ' + this.org.name + '/' + this.name);
|
||||
return client.repo(this.org.name + '/' + this.name);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Get contribution statistics for the repo.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceRepo.prototype.contributorsStatsOneTime = function (callback) {
|
||||
this.createGitHubRepoClient().contributorsStats(function (error, stats) {
|
||||
if (error) {
|
||||
var er = utils.wrapError(error, '');
|
||||
if (error && error.status && error.status == 202) {
|
||||
er.status = 202;
|
||||
}
|
||||
return callback(er);
|
||||
}
|
||||
callback(null, stats);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Add a collaborator with a specified permission level.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceRepo.prototype.addCollaborator = function (githubUsername, permissionLevel, callback) {
|
||||
if (typeof permissionLevel == 'function') {
|
||||
callback = permissionLevel;
|
||||
permissionLevel = 'pull';
|
||||
}
|
||||
this.createGitHubRepoClient().addCollaborator(githubUsername, {
|
||||
permission: permissionLevel,
|
||||
}, function(error, info) {
|
||||
if (error) {
|
||||
var message = error.statusCode == 404 ? 'The GitHub username "' + githubUsername + '" does not exist.' : 'The collaborator could not be added to GitHub at this time. There may be a problem with the GitHub API.';
|
||||
error.skipLog = error.statusCode == 404;
|
||||
return callback(utils.wrapError(error, message));
|
||||
}
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Remove a collaborator.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceRepo.prototype.removeCollaborator = function (githubUsername, callback) {
|
||||
var self = this;
|
||||
this.createGitHubRepoClient().removeCollaborator(githubUsername, function(error) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error, 'The collaborator could not be removed at this time. Was "' + githubUsername + '" even a collaborator for ' + self.name + '?'));
|
||||
}
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Get the list of collaborators for the repo from GitHub.
|
||||
// ----------------------------------------------------------------------------
|
||||
// CONSIDER: Use the Redis cache for this super hacky call.
|
||||
OpenSourceRepo.prototype.getOutsideCollaborators = function (callback) {
|
||||
var self = this;
|
||||
var client = this.createGitHubRepoClient();
|
||||
this.org.getAdministratorsHashCached(function (error, adminUsernamesToIds) {
|
||||
var administratorIds = {};
|
||||
for (var admin in adminUsernamesToIds) {
|
||||
administratorIds[adminUsernamesToIds[admin]] = true;
|
||||
}
|
||||
self.org.getAllMembersById(function (error, membersHash) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error, 'While looking up collaborators, we were not able to retrieve organization membership information.'));
|
||||
}
|
||||
utils.retrieveAllPages(client.collaborators.bind(client), function (error, collaborators) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error, 'We ran into a problem while trying to retrieve the collaborators for this repo.'));
|
||||
}
|
||||
async.map(collaborators, function (data, cb) {
|
||||
var rcp = data.permissions;
|
||||
delete data.permissions;
|
||||
var user = self.oss.user(data.id, data);
|
||||
user._repoCollaboratorPermissions = rcp;
|
||||
cb(null, user);
|
||||
}, function (error, collaboratorObjects) {
|
||||
// This is a workaround as suggested by GitHub.
|
||||
var corporateUsersToRemove = {};
|
||||
var corporateUsersWithCollaborationRights = [];
|
||||
async.each(collaboratorObjects, function (co, cb) {
|
||||
if (administratorIds[co.id]) {
|
||||
// Organization admin
|
||||
corporateUsersToRemove[co.id] = true;
|
||||
return cb();
|
||||
}
|
||||
if (membersHash[co.id]) {
|
||||
corporateUsersToRemove[co.id] = true;
|
||||
if (co._repoCollaboratorPermissions && co._repoCollaboratorPermissions.admin === true) {
|
||||
// This is a corporate user who has collaborator rights for this one.
|
||||
// We will still resolve the link.
|
||||
corporateUsersWithCollaborationRights.push(co);
|
||||
return co.getLink(function (ignored, link) {
|
||||
cb();
|
||||
});
|
||||
}
|
||||
}
|
||||
cb();
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
async.reject(collaboratorObjects, function (co, cb) {
|
||||
cb(corporateUsersToRemove[co.id] === true);
|
||||
}, function (results) {
|
||||
async.sortBy(results, function (entry, cb) {
|
||||
cb(null, entry.login);
|
||||
}, function (error, sorted) {
|
||||
callback(error, sorted, corporateUsersWithCollaborationRights);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Update the repo properties with a patch.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceRepo.prototype.update = function updateRepo(patch, callback) {
|
||||
// CONSIDER: Wrap errors.
|
||||
this.createGitHubRepoClient().update(patch, callback);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Delete the repo from GitHub.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceRepo.prototype.delete = function updateRepo(patch, callback) {
|
||||
// CONSIDER: Wrap errors.
|
||||
this.createGitHubRepoClient().destroy(callback);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve a repo-scoped issue object.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceRepo.prototype.issue = function getIssueInstance(issueNumber, optionalInitialData) {
|
||||
var self = this;
|
||||
if (typeof issueNumber != 'string') {
|
||||
issueNumber = issueNumber.toString();
|
||||
}
|
||||
if (self.inner.issues[issueNumber]) {
|
||||
return self.inner.issues[issueNumber];
|
||||
} else {
|
||||
self.inner.issues[issueNumber] = new Issue(self, issueNumber, optionalInitialData);
|
||||
return self.inner.issues[issueNumber];
|
||||
}
|
||||
};
|
||||
|
||||
// CONSIDER: OLD: Is this needed still?
|
||||
OpenSourceRepo.prototype.createIssue = function (issue, callback) {
|
||||
var fullName = this.full_name;
|
||||
var repositoryClient = this.oss.createGenericGitHubClient().repo(fullName);
|
||||
repositoryClient.createIssue(issue, function (error, createdIssue) {
|
||||
if (error) {
|
||||
error = utils.wrapError(error, 'We had trouble opening an issue to track this request in the ' + fullName + ' repo.');
|
||||
}
|
||||
callback(error, createdIssue);
|
||||
});
|
||||
};
|
||||
|
||||
// CONSIDER: OLD: Is this needed still?
|
||||
OpenSourceRepo.prototype.updateIssue = function (issueNumber, patch, callback) {
|
||||
var fullName = this.full_name;
|
||||
var issueClient = this.oss.createGenericGitHubClient().issue(this.full_name, issueNumber);
|
||||
issueClient.update(patch, function (error, updatedIssue) {
|
||||
if (error) {
|
||||
error = utils.wrapError(error, 'We had trouble updated the issue in the ' + fullName + ' repo.');
|
||||
}
|
||||
callback(error, updatedIssue);
|
||||
});
|
||||
};
|
||||
|
||||
function setDetails(self, details) {
|
||||
var key = null;
|
||||
for (var i = 0; i < detailsToCopy.length; i++) {
|
||||
key = detailsToCopy[i];
|
||||
self[key] = utils.stealValue(details, key);
|
||||
}
|
||||
for (i = 0; i < detailsToSkip.length; i++) {
|
||||
key = detailsToSkip[i];
|
||||
self.otherFields[key] = utils.stealValue(details, key);
|
||||
}
|
||||
for (var k in details) {
|
||||
debug('Repo details import, remaining key: ' + k);
|
||||
}
|
||||
self._detailsLoaded = true;
|
||||
}
|
||||
|
||||
module.exports = OpenSourceRepo;
|
|
@ -0,0 +1,464 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var async = require('async');
|
||||
var github = require('octonode');
|
||||
var debug = require('debug')('azureossportal');
|
||||
var utils = require('../utils');
|
||||
var OpenSourceRepo = require('./repo');
|
||||
|
||||
function OpenSourceOrganizationTeam (orgInstance, id, optionalInitialData) {
|
||||
if (!id) {
|
||||
throw new Error('No team ID was provided for construction.');
|
||||
}
|
||||
this.id = id;
|
||||
if (!orgInstance) {
|
||||
throw new Error('Required organization instance is missing.');
|
||||
}
|
||||
this.org = orgInstance;
|
||||
this.oss = orgInstance.oss;
|
||||
this.otherFields = {};
|
||||
this._detailsLoaded = false;
|
||||
if (optionalInitialData) {
|
||||
setDetails(this, optionalInitialData);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Properties of interest in the standard GitHub response for a team
|
||||
// ----------------------------------------------------------------------------
|
||||
var detailsToCopy = [
|
||||
'name',
|
||||
'slug',
|
||||
'description',
|
||||
'permission',
|
||||
'url',
|
||||
'members_url',
|
||||
'repositories_url',
|
||||
'members_count',
|
||||
'repos_count',
|
||||
'privacy',
|
||||
];
|
||||
var detailsToSkip = [
|
||||
'id',
|
||||
'organization',
|
||||
];
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Creates a GitHub (octonode) API client for this team ID.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.createGitHubTeamClient = function () {
|
||||
var source = this[this.org ? 'org' : 'oss'];
|
||||
var method = source.createGenericGitHubClient;
|
||||
if (method === undefined) {
|
||||
throw new Error('Unable to find the GitHub client factory associated with the team.');
|
||||
}
|
||||
var client = method.call(source);
|
||||
return client.team(this.id);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Get the team details
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.getDetails = function queryTeamDetails(callback) {
|
||||
var self = this;
|
||||
self.createGitHubTeamClient().info(function (error, info) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error, 'We were unable to retrieve information about team ID ' + self.id + '.'));
|
||||
}
|
||||
var copy = {};
|
||||
utils.merge(copy, info);
|
||||
if (!self.org && info.organization && info.organization.login) {
|
||||
self.org = self.oss.org(info.organization.login);
|
||||
}
|
||||
setDetails(self, info); // destructive operation
|
||||
callback(null, copy);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Set the team details
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.setDetails = function setDetailsExternal(details) {
|
||||
if (details.id == this.id) {
|
||||
setDetails(this, details);
|
||||
} else {
|
||||
throw new Error('The provided details object does not reference team ID ' + this.id);
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Delete the team.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.delete = function deleteTeam(callback) {
|
||||
this.createGitHubTeamClient().destroy(function (error) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error, 'We were unable to delete team ID ' + self.id + ' using the GitHub API.'));
|
||||
}
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Update specific team details. Also updates the local copies in case the same
|
||||
// request needs to show the updated info.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.update = function updateTeamDetails(updates, callback) {
|
||||
var self = this;
|
||||
self.createGitHubTeamClient().update(updates, function (error, info) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error, 'We were unable to update team ID ' + self.id + ' using the GitHub API.'));
|
||||
}
|
||||
var copy = {};
|
||||
utils.merge(copy, updates);
|
||||
setDetails(self, info); // destructive operation
|
||||
// Clear the org's cache in case the team was renamed...
|
||||
self.org.clearTeamsCache(callback);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Delete the team.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.delete = function deleteTeam(callback) {
|
||||
var self = this;
|
||||
self.createGitHubTeamClient().destroy(function (error) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error, 'We were unable to destroy the team ID ' + self.id + ' via the GitHub API.'));
|
||||
}
|
||||
self.org.clearTeamsCache(callback);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Ensure that we have team details.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.ensureDetailsAndOrganization = function insurance(callback) {
|
||||
var self = this;
|
||||
var ensureOrganizationReference = function (cb) {
|
||||
if (!self.org) {
|
||||
if (self.otherFields.organization && self.otherFields.organization.login) {
|
||||
var orgName = self.otherFields.organization.login;
|
||||
self.org = self.oss.org(orgName);
|
||||
} else {
|
||||
return cb(new Error('The name of the organization for a team could not be retrieved logically.'));
|
||||
}
|
||||
}
|
||||
cb();
|
||||
};
|
||||
if (!self._detailsLoaded) {
|
||||
self.getDetails(function (error) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
ensureOrganizationReference(callback);
|
||||
});
|
||||
} else {
|
||||
ensureOrganizationReference(callback);
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Add a repo and permission level to a GitHub team.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.addRepository = function addRepo(repoName, permission, callback) {
|
||||
this.org.createGenericGitHubClient().org(this.org.name).addTeamRepo(this.id, repoName, {
|
||||
permission: permission
|
||||
}, callback);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Get the repos managed by the team.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.getRepos = function queryRepos(callback) {
|
||||
var self = this;
|
||||
var ghteam = self.createGitHubTeamClient();
|
||||
// CONSIDER: GitHub API can let y ou filter for just org-owned repos now...
|
||||
utils.retrieveAllPages(ghteam.repos.bind(ghteam), function (error, repos) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
async.filter(repos, function (repo, cb) {
|
||||
cb(repo && repo.owner && repo.owner.login && repo.owner.login.toLowerCase() == self.org.name.toLowerCase());
|
||||
}, function (repos) {
|
||||
async.map(repos, function (repo, cb) {
|
||||
cb(null, new OpenSourceRepo(self.org, repo.name, repo));
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Check for public membership
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.isMember = function queryTeamMembership(callback) {
|
||||
var self = this;
|
||||
var username = self.oss.usernames.github;
|
||||
self.createGitHubTeamClient().membership(username, function (error, result) {
|
||||
return callback(null, result === true);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Add membership for the authenticated user OR another GitHub username
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.addMembership = function addTeamMembership(role, optionalUsername, callback) {
|
||||
var self = this;
|
||||
if (!(role == 'member' || role == 'maintainer')) {
|
||||
return callback(new Error('The provided role type "' + role + '" is not supported at this time.'));
|
||||
}
|
||||
if (typeof optionalUsername == 'function') {
|
||||
callback = optionalUsername;
|
||||
optionalUsername = self.oss.usernames.github;
|
||||
}
|
||||
var options = {
|
||||
role: role
|
||||
};
|
||||
self.createGitHubTeamClient().addMembership(optionalUsername, options, function (error, obj) {
|
||||
if (error) {
|
||||
callback(error);
|
||||
} else {
|
||||
clearRedisKeysAfterMembershipChange(self, function () {
|
||||
callback(null, obj);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Remove membership for the authenticated user OR another GitHub username
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.removeMembership = function removeTeamMembership(optionalUsername, callback) {
|
||||
var self = this;
|
||||
if (typeof optionalUsername == 'function') {
|
||||
callback = optionalUsername;
|
||||
optionalUsername = this.oss.usernames.github;
|
||||
}
|
||||
this.createGitHubTeamClient().removeMembership(optionalUsername, function (error) {
|
||||
if (!error) {
|
||||
clearRedisKeysAfterMembershipChange(self, callback);
|
||||
} else {
|
||||
callback(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function clearRedisKeysAfterMembershipChange(self, silentCallback) {
|
||||
var keys = [
|
||||
'team#' + self.id + '(all)',
|
||||
'team#' + self.id + '(member)',
|
||||
'team#' + self.id + '(maintainer)',
|
||||
];
|
||||
async.each(keys, function (key, cb) {
|
||||
self.oss.redis.delete(key, cb);
|
||||
}, function () {
|
||||
if (silentCallback) {
|
||||
silentCallback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieves the members of the team.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.getMembers = function getMembers(optionalRole, callback) {
|
||||
var self = this;
|
||||
var params = null;
|
||||
if (typeof optionalRole == 'function') {
|
||||
callback = optionalRole;
|
||||
optionalRole = null;
|
||||
} else {
|
||||
params = {
|
||||
role: optionalRole
|
||||
};
|
||||
}
|
||||
var ghteam = this.createGitHubTeamClient();
|
||||
utils.retrieveAllPages(ghteam.members.bind(ghteam), params, function (error, members) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
// Update the cache for this team
|
||||
var redisKey = 'team#' + self.id + '(' + optionalRole + ')';
|
||||
var randomExpireMinutes = utils.randomInteger(240, 60 * 24 * 2 /* 2 days max */);
|
||||
self.oss.redis.setObjectWithExpire(redisKey, members, randomExpireMinutes, function () {
|
||||
async.map(members, function (member, cb) {
|
||||
cb(null, self.oss.user(member.id, member));
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieves the members of the team. This is a fork of the getMembers method
|
||||
// that does explicit Redis caching when available. For now, forked to avoid
|
||||
// confusion.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.getMembersCached = function getMembersCached(requiredRole, callback) {
|
||||
var self = this;
|
||||
if (typeof requiredRole == 'function') {
|
||||
return callback(new Error('getMembersCached requires a role.'));
|
||||
}
|
||||
var instancesFromJson = function (members) {
|
||||
async.map(members, function (member, cb) {
|
||||
cb(null, self.oss.user(member.id, member));
|
||||
}, callback);
|
||||
};
|
||||
var lightweightFieldsToPreserve = ['login', 'id'];
|
||||
var params = {
|
||||
role: requiredRole
|
||||
};
|
||||
var redisKey = 'team#' + self.id + '(' + requiredRole + ')';
|
||||
var ghteam = this.createGitHubTeamClient();
|
||||
self.oss.redis.getObject(redisKey, function (error, data) {
|
||||
if (!error && data) {
|
||||
return instancesFromJson(data);
|
||||
}
|
||||
utils.retrieveAllPages(ghteam.members.bind(ghteam), params, function (error, members) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
async.map(members, function (member, cb) {
|
||||
var lw = {};
|
||||
for (var i = 0; i < lightweightFieldsToPreserve.length; i++) {
|
||||
lw[lightweightFieldsToPreserve[i]] = member[lightweightFieldsToPreserve[i]];
|
||||
}
|
||||
cb(null, lw);
|
||||
}, function (error, lightweightMembers) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
var randomExpireMinutes = utils.randomInteger(240, 60 * 24 * 2 /* 2 days max */);
|
||||
self.oss.redis.setObjectWithExpire(redisKey, lightweightMembers, randomExpireMinutes, function () {
|
||||
instancesFromJson(lightweightMembers);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieves the members of the team.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.getMemberLinks = function getMembersAndLinks(callback) {
|
||||
var self = this;
|
||||
this.getMembers(function (error, members) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
if (members.length && members.length > 0) {
|
||||
self.oss.getLinksForUsers(members, callback);
|
||||
} else {
|
||||
callback(null, []);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieves the maintainers of the team.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.getMaintainers = function getMaintainers(callback) {
|
||||
this.getMembers('maintainer', callback);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieves the maintainers of the team, including fallback logic,
|
||||
// in the case there are no explicit maintainers, we go to the organization's
|
||||
// sudoers - a special team where any member of that specific team is granted
|
||||
// special portal abilities. In the case that this organization does not have
|
||||
// any sudoers defined, and this is a leaf node org, then the sudoers from the
|
||||
// primary org will be appointed the official maintainers for this team. This
|
||||
// function also loads the links from the underlying data system to be able to
|
||||
// provide robust information about the users, including their corporate
|
||||
// relationship.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.getOfficialMaintainers = function (callback) {
|
||||
var self = this;
|
||||
self.ensureDetailsAndOrganization(function (error) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
self.getMaintainers(function (error, maintainers) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
if (maintainers.length > 0) {
|
||||
self.oss.getLinksForUsers(maintainers, callback);
|
||||
} else {
|
||||
// Better design here would be to then fallback to the org obj. to get members themselves.
|
||||
var team = self.org.getSudoersTeam();
|
||||
team.getMembers(function (error, members) {
|
||||
if (!error && members && members.length === 0) {
|
||||
error = new Error('No official organization approvers could be retrieved.');
|
||||
}
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
self.oss.getLinksForUsers(members, callback);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieves pending approvals for this specific team and hydrates user links
|
||||
// and accounts. It is possible that errors could happen if a user were to
|
||||
// rename their GitHub account after submitting a request since the request's
|
||||
// copy of the login is used for link hydration.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceOrganizationTeam.prototype.getApprovals = function (callback) {
|
||||
var self = this;
|
||||
var dc = this.oss.dataClient();
|
||||
dc.getPendingApprovals(this.id, function (error, pendingApprovals) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error, 'We were unable to retrieve the pending approvals list for this team. There may be a data store problem.'));
|
||||
}
|
||||
var requestingUsers = {};
|
||||
async.each(pendingApprovals, function (approval, cb) {
|
||||
requestingUsers[approval.ghu] = approval.ghid;
|
||||
if (approval.requested) {
|
||||
var asInt = parseInt(approval.requested, 10);
|
||||
approval.requestedTime = new Date(asInt);
|
||||
}
|
||||
cb();
|
||||
}, function () {
|
||||
self.oss.getCompleteUsersFromUsernameIdHash(requestingUsers, function (error, users) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
async.each(pendingApprovals, function (approval, cb) {
|
||||
var login = approval.ghu;
|
||||
if (users[login]) {
|
||||
approval.completeRequestingUser = users[login];
|
||||
}
|
||||
cb();
|
||||
}, function (error) {
|
||||
callback(error, pendingApprovals);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// PRIVATE FUNCTIONS
|
||||
|
||||
function setDetails(team, details) {
|
||||
var self = team;
|
||||
var key = null;
|
||||
for (var i = 0; i < detailsToCopy.length; i++) {
|
||||
key = detailsToCopy[i];
|
||||
self[key] = utils.stealValue(details, key);
|
||||
}
|
||||
for (i = 0; i < detailsToSkip.length; i++) {
|
||||
key = detailsToSkip[i];
|
||||
self.otherFields[key] = utils.stealValue(details, key);
|
||||
}
|
||||
for (var k in details) {
|
||||
debug('Team details import, remaining key: ' + k);
|
||||
}
|
||||
self._detailsLoaded = true;
|
||||
}
|
||||
|
||||
module.exports = OpenSourceOrganizationTeam;
|
|
@ -0,0 +1,272 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var async = require('async');
|
||||
var github = require('octonode');
|
||||
var utils = require('../utils');
|
||||
var debug = require('debug')('azureossportal');
|
||||
|
||||
function OpenSourceUser (ossInstance, githubId, optionalGitHubInstance) {
|
||||
this.id = githubId;
|
||||
this.oss = ossInstance;
|
||||
this.otherFields = {};
|
||||
this.link = null;
|
||||
this._detailsLoaded = false;
|
||||
if (optionalGitHubInstance) {
|
||||
setDetails(this, optionalGitHubInstance);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Properties of interest in the standard GitHub response for a user
|
||||
// ----------------------------------------------------------------------------
|
||||
var detailsToCopy = [
|
||||
'login',
|
||||
'avatar_url',
|
||||
// only in detailed info responses:
|
||||
'name',
|
||||
'company',
|
||||
'location',
|
||||
'email',
|
||||
'bio',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
var detailsToSkip = [
|
||||
'id',
|
||||
'gravatar_id',
|
||||
'url',
|
||||
'html_url',
|
||||
'followers_url',
|
||||
'following_url',
|
||||
'gists_url',
|
||||
'starred_url',
|
||||
'subscriptions_url',
|
||||
'organizations_url',
|
||||
'repos_url',
|
||||
'events_url',
|
||||
'received_events_url',
|
||||
'type',
|
||||
'site_admin',
|
||||
// only in detailed info responses:
|
||||
'blog',
|
||||
'hireable',
|
||||
'public_repos',
|
||||
'public_gists',
|
||||
'followers',
|
||||
'following',
|
||||
// organizations:
|
||||
'members_url',
|
||||
'public_members_url',
|
||||
'description',
|
||||
'total_private_repos',
|
||||
'owned_private_repos',
|
||||
'private_gists',
|
||||
'disk_usage',
|
||||
'collaborators',
|
||||
'billing_email',
|
||||
'plan',
|
||||
// when in the context of a collaborators response only:
|
||||
'permissions',
|
||||
];
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve the link contact information, if the link has been loaded.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUser.prototype.contactEmail = function () {
|
||||
return this.link ? this.link.aadupn : null;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve the link contact information alias subset, if link loaded.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUser.prototype.corporateAlias = function () {
|
||||
if (this.link && this.link.aadupn) {
|
||||
var email = this.link.aadupn;
|
||||
var i = email.indexOf('@');
|
||||
if (i >= 0) {
|
||||
return email.substring(0, i);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve the link contact information alias subset, if link loaded.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUser.prototype.corporateProfileUrl = function () {
|
||||
var alias = this.corporateAlias();
|
||||
var prefix = this.oss.setting('corporate').userProfilePrefix;
|
||||
if (alias && prefix) {
|
||||
return prefix + alias;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve the link contact information, if the link has been loaded.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUser.prototype.contactName = function () {
|
||||
if (this.link) {
|
||||
return this.link.aadname || this.login;
|
||||
}
|
||||
return this.login;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieves the URL for the user's avatar, if present. If the user's details
|
||||
// have not been loaded, we will not yet have an avatar URL.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUser.prototype.avatar = function (optionalSize) {
|
||||
if (!optionalSize) {
|
||||
optionalSize = 80;
|
||||
}
|
||||
if (this.avatar_url) {
|
||||
return this.avatar_url + '&s=' + optionalSize;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieve the link, if any, for this user from the underlying datastore. Will
|
||||
// cache the value in memory for this instance, since the lifetime of these
|
||||
// objects is a single request.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUser.prototype.getLink = function (callback) {
|
||||
if (this.link) {
|
||||
return callback(null, this.link);
|
||||
}
|
||||
var self = this;
|
||||
var dc = self.oss.dataClient();
|
||||
dc.getLink(self.id, function (error, link) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error, 'We were not able to retrieve information about the link for user ' + self.id + ' at this time.'));
|
||||
}
|
||||
self.link = (link === false) ? false : dc.reduceEntity(link);
|
||||
callback(null, self.link);
|
||||
});
|
||||
};
|
||||
|
||||
OpenSourceUser.prototype.getLinkRequired = function (callback) {
|
||||
var self = this;
|
||||
this.getLink(function (error) {
|
||||
if (!error && self.link === false) {
|
||||
error = new Error('No link retrieved.');
|
||||
}
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
callback(null, self.link);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Special-use function to set the link when provided elsewhere. This is
|
||||
// helpful since we can efficiently query a large set of links for team list
|
||||
// scenarios and then set them here.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUser.prototype.setLinkInstance = function (links, optionalSuppressDebug) {
|
||||
if (!Array.isArray(links)) {
|
||||
links = [links];
|
||||
}
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
var link = links[i];
|
||||
if (link.ghid === this.id) {
|
||||
this.link = link;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!this.link && optionalSuppressDebug !== true) {
|
||||
throw new Error('No matching link was provided for the user ID ' + this.id + '.');
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Special-use function to set the link when provided elsewhere. This is
|
||||
// helpful since we can efficiently query a large set of links for team list
|
||||
// scenarios and then set them here. Captures a throw and ignores the issue.
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUser.prototype.trySetLinkInstance = function (links, optionalSuppressDebug) {
|
||||
try {
|
||||
this.setLinkInstance(links, optionalSuppressDebug);
|
||||
} catch (error) {
|
||||
debug('trySetLinkInstance: No link exists for user ' + this.id);
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Load the GitHub details for the user.
|
||||
// Problem: we have the ID, but GitHub cheaply prefers usernames, not IDs...
|
||||
// ----------------------------------------------------------------------------
|
||||
OpenSourceUser.prototype.getDetailsByUsername = function (login, callback) {
|
||||
var self = this;
|
||||
var username = this.login;
|
||||
if (typeof login == 'function') {
|
||||
callback = login;
|
||||
login = null;
|
||||
} else {
|
||||
username = login;
|
||||
}
|
||||
if (!username) {
|
||||
return callback(new Error('No username provided for retrieving the details of user ' + self.id));
|
||||
}
|
||||
self.oss.createGenericGitHubClient().user(username).info(function (error, info) {
|
||||
if (error) {
|
||||
return callback(utils.wrapError(error, 'We were unable to retrieve information about user ' + username + ' (' + self.id + ').'));
|
||||
}
|
||||
var copy = {};
|
||||
utils.merge(copy, info);
|
||||
setDetails(self, info); // destructive operation
|
||||
callback(null, copy);
|
||||
});
|
||||
};
|
||||
|
||||
OpenSourceUser.prototype.getProfileCreatedDate = function () {
|
||||
if (this.created_at) {
|
||||
return new Date(this.created_at);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
OpenSourceUser.prototype.getProfileUpdatedDate = function () {
|
||||
if (this.updated_at) {
|
||||
return new Date(this.updated_at);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
OpenSourceUser.prototype.debugView = function () {
|
||||
var obj = {};
|
||||
for (var key in this) {
|
||||
var val = this[key];
|
||||
if (typeof val == 'string') {
|
||||
obj[key] = val;
|
||||
} else {
|
||||
if (key == 'otherFields' || key == 'link' || key == 'bio') {
|
||||
obj[key] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
function setDetails(self, details) {
|
||||
var key = null;
|
||||
for (var i = 0; i < detailsToCopy.length; i++) {
|
||||
key = detailsToCopy[i];
|
||||
self[key] = utils.stealValue(details, key);
|
||||
}
|
||||
for (i = 0; i < detailsToSkip.length; i++) {
|
||||
key = detailsToSkip[i];
|
||||
self.otherFields[key] = utils.stealValue(details, key);
|
||||
}
|
||||
for (var k in details) {
|
||||
debug('User details import, remaining key: ' + k);
|
||||
}
|
||||
self._detailsLoaded = true;
|
||||
}
|
||||
|
||||
module.exports = OpenSourceUser;
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"name": "open-source-portal-for-github",
|
||||
"author": "Microsoft Corporation",
|
||||
"contributors": [
|
||||
"Wilcox, Jeff <jwilcox@microsoft.com>"
|
||||
],
|
||||
"version": "3.0.2",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"keywords": [
|
||||
"github",
|
||||
"management",
|
||||
"organization"
|
||||
],
|
||||
"tags": [
|
||||
"github",
|
||||
"node",
|
||||
"management",
|
||||
"organization"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "node ./bin/www",
|
||||
"jshint": "jshint ."
|
||||
},
|
||||
"engines": {
|
||||
"node": "~4.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"applicationinsights": "^0.12.5",
|
||||
"async": "^1.4.2",
|
||||
"azure-storage": "^0.6.0",
|
||||
"body-parser": "~1.14.1",
|
||||
"compression": "^1.6.0",
|
||||
"connect-redis": "^3.0.0",
|
||||
"cookie-parser": "~1.4.0",
|
||||
"debug": "~2.2.0",
|
||||
"express": "~4.13.3",
|
||||
"express-session": "^1.11.3",
|
||||
"jade": "1.11.0",
|
||||
"moment": "^2.10.6",
|
||||
"morgan": "~1.6.1",
|
||||
"node-uuid": "^1.4.3",
|
||||
"octonode": "pksunkara/octonode",
|
||||
"passport": "^0.3.0",
|
||||
"passport-azure-ad": "^1.3.6",
|
||||
"passport-github": "^1.0.0",
|
||||
"redis": "^2.2.3",
|
||||
"serve-favicon": "~2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jshint": "^2.8.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
</msapplication>
|
||||
</browserconfig>
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011-2015 Twitter, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,548 @@
|
|||
/*
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
*/
|
||||
|
||||
body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div#wiki-toolbar {
|
||||
margin-top:12px;
|
||||
}
|
||||
|
||||
.general-margin-left {
|
||||
margin-left:12px;
|
||||
}
|
||||
|
||||
div#content {
|
||||
margin-top:24px;
|
||||
}
|
||||
|
||||
.container-pad {
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
.person-tile {
|
||||
display: inline-block;
|
||||
padding: 4px;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
.capitalize {
|
||||
text-transform:capitalize;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
background-color: #fff;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.a-unstyled { color: #333; }
|
||||
.a-unstyled:link { color: #333; }
|
||||
.a-unstyled:visited { color: #333; }
|
||||
.a-unstyled:hover { color: #000; text-decoration: none; }
|
||||
.a-unstyled:active { color: #333; }
|
||||
|
||||
/* first row navigation styling */
|
||||
.first-row-nav {
|
||||
background-color: #fff;
|
||||
color: #111;
|
||||
border: 0;
|
||||
}
|
||||
.first-row-nav a.navbar-brand {
|
||||
color: #333;
|
||||
}
|
||||
.first-row-nav a.navbar-brand:hover {
|
||||
color: #444;
|
||||
}
|
||||
.first-row-nav .navbar-nav > li > a {
|
||||
color: #555;
|
||||
}
|
||||
.first-row-nav .navbar-nav > li > a:hover,
|
||||
.first-row-nav .navbar-nav > .active > a,
|
||||
.first-row-nav .navbar-nav > .active > a:hover {
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
}
|
||||
.first-row-nav .navbar-toggle .icon-bar {
|
||||
background-color: #555;
|
||||
}
|
||||
.first-row-nav .navbar-header .navbar-toggle {
|
||||
border-color: #fff;
|
||||
}
|
||||
.first-row-nav .navbar-header .navbar-toggle:hover,
|
||||
.first-row-nav .navbar-header .navbar-toggle:focus {
|
||||
background-color: #f9f9f9;
|
||||
border-color: #f9f9f9;
|
||||
}
|
||||
|
||||
/* second row sub navigation for the portal */
|
||||
.navbar.second-row-nav {
|
||||
margin-top: -21px;
|
||||
padding-bottom: 2px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.second-row-nav {
|
||||
background-color: #eee;
|
||||
color: #111;
|
||||
border: 0;
|
||||
}
|
||||
.second-row-nav a.navbar-brand {
|
||||
color: #333;
|
||||
}
|
||||
.second-row-nav a.navbar-brand:hover {
|
||||
color: #444;
|
||||
}
|
||||
.second-row-nav .navbar-nav > li > a {
|
||||
color: #555;
|
||||
}
|
||||
.second-row-nav .navbar-nav > li > a:hover,
|
||||
.second-row-nav .navbar-nav > .active > a,
|
||||
.second-row-nav .navbar-nav > .active > a:hover {
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
}
|
||||
.second-row-nav .navbar-toggle .icon-bar {
|
||||
background-color: #555;
|
||||
}
|
||||
.second-row-nav .navbar-header .navbar-toggle {
|
||||
border-color: #fff;
|
||||
}
|
||||
.second-row-nav .navbar-header .navbar-toggle:hover,
|
||||
.second-row-nav .navbar-header .navbar-toggle:focus {
|
||||
background-color: #f9f9f9;
|
||||
border-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.btn-huge {
|
||||
margin:24px 0px;
|
||||
padding:24px 36px;
|
||||
font-size:32pt;
|
||||
}
|
||||
|
||||
h1.huge {
|
||||
font-size: 68px;
|
||||
}
|
||||
|
||||
h2 strong {
|
||||
font-family: "Segoe UI Semibold", Segoe, "Segoe WP", Calibri, Candara, Tahoma, Verdana, Arial, sans-serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* custom color button for a more muted appearance */
|
||||
.btn-default {
|
||||
color:#fff;
|
||||
background-color: #444;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
.btn-muted {
|
||||
color:#333;
|
||||
background-color: #bbb;
|
||||
border-color: #bbb;
|
||||
}
|
||||
.btn-muted:hover,
|
||||
.btn-muted:focus,
|
||||
.btn-muted:active,
|
||||
.btn-muted.active,
|
||||
.open > .dropdown-toggle.btn-default {
|
||||
color: #000;
|
||||
background-color: #999;
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.btn-muted-more {
|
||||
color:#777;
|
||||
background-color: #ddd;
|
||||
border-color: #ddd;
|
||||
}
|
||||
.btn-muted-more:hover,
|
||||
.btn-muted-more:focus,
|
||||
.btn-muted-more:active,
|
||||
.btn-muted-more.active,
|
||||
.open > .dropdown-toggle.btn-default {
|
||||
color: #000;
|
||||
background-color: #ccc;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
.btn-white {
|
||||
color:#333;
|
||||
background-color: #fff;
|
||||
border-color: #fff;
|
||||
}
|
||||
.btn-white:hover,
|
||||
.btnd-white:focus,
|
||||
.btnd-white:active,
|
||||
.btnd-white.active,
|
||||
.open > .dropdown-toggle.btn-default {
|
||||
color: #fff;
|
||||
background-color: #0078d7;
|
||||
border-color: #0078d7;
|
||||
}
|
||||
|
||||
/* wiki editing tools */
|
||||
textarea#editor-body {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 24px;
|
||||
width: 100%;
|
||||
min-height: 480px;
|
||||
font-family: Consolas, Courier, "Courier New";
|
||||
}
|
||||
|
||||
/* general styling for the wiki */
|
||||
footer {
|
||||
margin-top: 36px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
code {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.alert-gray {
|
||||
background-color: #eee;
|
||||
border-color: #ddd;
|
||||
color: #777;
|
||||
}
|
||||
.alert-gray hr {
|
||||
border-top-color: #999;
|
||||
}
|
||||
.alert-gray .alert-link {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
input#search-box {
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
height: 36px;
|
||||
margin-top: 4px;
|
||||
max-width:160px;
|
||||
}
|
||||
|
||||
.sevenpercent {
|
||||
width:7%;
|
||||
}
|
||||
.fivepercent {
|
||||
width:5%;
|
||||
}
|
||||
.tenpercent {
|
||||
width:10%;
|
||||
}
|
||||
.fifteenpercent {
|
||||
width:15%;
|
||||
}
|
||||
.twentypercent {
|
||||
width:20%;
|
||||
}
|
||||
.twentyfivepercent {
|
||||
width:25%;
|
||||
}
|
||||
.thirtypercent {
|
||||
width:30%;
|
||||
}
|
||||
|
||||
div.alert {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
div.alerts {
|
||||
margin: 0;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
/* theme colors and components */
|
||||
div.metro-box {
|
||||
position:relative;
|
||||
margin:0px;
|
||||
width:100%;
|
||||
}
|
||||
.metro-box a {
|
||||
padding:1.4em 25px;
|
||||
display: inline-block;
|
||||
color:#fff;
|
||||
font-size: .9em;
|
||||
position:relative;
|
||||
width:100%;
|
||||
}
|
||||
.metro-box a h3 {
|
||||
color: #fff;
|
||||
}
|
||||
.metro-box a p {
|
||||
color: #fff;
|
||||
}
|
||||
.metro-box a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div.link-box {
|
||||
position:relative;
|
||||
margin:0px;
|
||||
width:100%;
|
||||
}
|
||||
.link-box a {
|
||||
padding:.4em 25px;
|
||||
display: inline-block;
|
||||
position:relative;
|
||||
width:100%;
|
||||
}
|
||||
.link-box a:hover {
|
||||
text-decoration: none;
|
||||
background-color: #efefef;
|
||||
}
|
||||
.link-box p.lead {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
/* Brand box coloring v1 */
|
||||
.metro-blue {
|
||||
background:#0072C6;
|
||||
}
|
||||
.metro-purple {
|
||||
background:#68217A;
|
||||
}
|
||||
.metro-gray {
|
||||
background:#666;
|
||||
}
|
||||
.metro-orange {
|
||||
background:#fa6800;
|
||||
}
|
||||
|
||||
/* Core brand-related colors */
|
||||
.ms-yellow {
|
||||
background:#ffb900;
|
||||
}
|
||||
.ms-orange {
|
||||
background:#d83b01;
|
||||
}
|
||||
.ms-red {
|
||||
background:#e81123;
|
||||
}
|
||||
.ms-magenta {
|
||||
background:#b4009e;
|
||||
}
|
||||
.ms-purple {
|
||||
background:#5c2d91;
|
||||
}
|
||||
.ms-blue {
|
||||
background:#0078d7;
|
||||
}
|
||||
.ms-teal {
|
||||
background:#008272;
|
||||
}
|
||||
.ms-green {
|
||||
background:#107c10;
|
||||
}
|
||||
/* Additional brand colors */
|
||||
.ms-light-orange {
|
||||
background:#ff8c00;
|
||||
}
|
||||
.ms-light-magenta {
|
||||
background:#e3008c;
|
||||
}
|
||||
.ms-light-purple {
|
||||
background:#b4a0ff;
|
||||
}
|
||||
.ms-light-blue {
|
||||
background:#00bcf2;
|
||||
}
|
||||
.ms-light-teal {
|
||||
background:#00b294;
|
||||
}
|
||||
.ms-light-green {
|
||||
background:#bad80a;
|
||||
}
|
||||
.ms-light-yellow {
|
||||
background:#fff100;
|
||||
}
|
||||
.ms-dark-red {
|
||||
background:#a80000;
|
||||
}
|
||||
.ms-dark-magenta {
|
||||
background:#5c005c;
|
||||
}
|
||||
.ms-dark-purple {
|
||||
background:#32145a;
|
||||
}
|
||||
.ms-mid-blue {
|
||||
background:#002050;
|
||||
}
|
||||
.ms-dark-blue {
|
||||
background:#002050;
|
||||
}
|
||||
.ms-dark-teal {
|
||||
background:#004b50;
|
||||
}
|
||||
.ms-dark-green {
|
||||
background:#004b1c;
|
||||
}
|
||||
.ms-white {
|
||||
background:#fff;
|
||||
}
|
||||
.ms-light-gray {
|
||||
background:#d2d2d2;
|
||||
}
|
||||
.ms-mid-gray {
|
||||
background:#737373;
|
||||
}
|
||||
.ms-dark-gray {
|
||||
background:#505050;
|
||||
}
|
||||
.ms-rich-black {
|
||||
background:#000;
|
||||
}
|
||||
/* lighter boxes need dark foreground colors */
|
||||
.ms-yellow a, .ms-light-orange a, .ms-light-magenta a, .ms-light-purple a, .ms-light-blue a,
|
||||
.ms-light-teal a, .ms-light-green a, .ms-light-yellow a, .ms-white a, .ms-light-gray a {
|
||||
color:#000;
|
||||
}
|
||||
.ms-yellow a h3, .ms-light-orange a h3, .ms-light-magenta a h3, .ms-light-purple a h3, .ms-light-blue a h3,
|
||||
.ms-light-teal a h3, .ms-light-green a h3, .ms-light-yellow a h3, .ms-white a h3, .ms-light-gray a h3 {
|
||||
color:#000;
|
||||
}
|
||||
.ms-yellow a p, .ms-light-orange a p, .ms-light-magenta a p, .ms-light-purple a p, .ms-light-blue a p,
|
||||
.ms-light-teal a p, .ms-light-green a p, .ms-light-yellow a p, .ms-white a p, .ms-light-gray a p {
|
||||
color:#000;
|
||||
}
|
||||
|
||||
/* table width modifiers */
|
||||
th.w-25, td.w-25 {
|
||||
width:30%;
|
||||
}
|
||||
th.w-20, td.w-20 {
|
||||
width:20%;
|
||||
}
|
||||
td.w-15, th.w-15 {
|
||||
width:15%;
|
||||
}
|
||||
td.w-10, th.w-10 {
|
||||
width:10%;
|
||||
}
|
||||
|
||||
/* By default it's not affixed in mobile views, so undo that */
|
||||
.wiki-sidebar.affix {
|
||||
position: static;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.wiki-sidebar {
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* First level of nav */
|
||||
.wiki-sidenav {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* All levels of nav */
|
||||
.wiki-sidebar .nav > li > a {
|
||||
display: block;
|
||||
padding: 4px 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #999;
|
||||
}
|
||||
.wiki-sidebar .nav > li > a:hover,
|
||||
.wiki-sidebar .nav > li > a:focus {
|
||||
padding-left: 19px;
|
||||
color: #0072C6;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
border-left: 1px solid #0072C6;
|
||||
}
|
||||
.wiki-sidebar .nav > .active > a,
|
||||
.wiki-sidebar .nav > .active:hover > a,
|
||||
.wiki-sidebar .nav > .active:focus > a {
|
||||
padding-left: 18px;
|
||||
font-weight: bold;
|
||||
color: #0072C6;
|
||||
background-color: transparent;
|
||||
border-left: 2px solid #0072C6;
|
||||
}
|
||||
|
||||
/* Nav: second level (shown on .active) */
|
||||
.wiki-sidebar .nav .nav {
|
||||
display: none; /* Hide by default, but at >768px, show it */
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.wiki-sidebar .nav .nav > li > a {
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
padding-left: 30px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.wiki-sidebar .nav .nav > li > a:hover,
|
||||
.wiki-sidebar .nav .nav > li > a:focus {
|
||||
padding-left: 29px;
|
||||
}
|
||||
.wiki-sidebar .nav .nav > .active > a,
|
||||
.wiki-sidebar .nav .nav > .active:hover > a,
|
||||
.wiki-sidebar .nav .nav > .active:focus > a {
|
||||
padding-left: 28px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Back to top (hidden on mobile) */
|
||||
.back-to-top {
|
||||
display: none;
|
||||
padding: 4px 10px;
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #999;
|
||||
}
|
||||
.back-to-top:hover {
|
||||
color: #0072C6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.back-to-top {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Show and affix the side nav when space allows it */
|
||||
@media (min-width: 992px) {
|
||||
.wiki-sidebar .nav > .active > ul {
|
||||
display: block;
|
||||
}
|
||||
/* Widen the fixed sidebar */
|
||||
.wiki-sidebar.affix,
|
||||
.wiki-sidebar.affix-bottom {
|
||||
width: 213px;
|
||||
}
|
||||
.wiki-sidebar.affix {
|
||||
position: fixed; /* Undo the static from mobile first approach */
|
||||
top: 20px;
|
||||
}
|
||||
.wiki-sidebar.affix-bottom {
|
||||
position: absolute; /* Undo the static from mobile first approach */
|
||||
}
|
||||
.wiki-sidebar.affix-bottom .wiki-sidenav,
|
||||
.wiki-sidebar.affix .wiki-sidenav {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
/* Widen the fixed sidebar again */
|
||||
.wiki-sidebar.affix-bottom,
|
||||
.wiki-sidebar.affix {
|
||||
width: 263px;
|
||||
}
|
||||
}
|
||||
|
||||
/* IE10 bugs */
|
||||
@-webkit-viewport { width: device-width; }
|
||||
@-moz-viewport { width: device-width; }
|
||||
@-ms-viewport { width: device-width; }
|
||||
@-o-viewport { width: device-width; }
|
||||
@viewport { width: device-width; }
|
После Ширина: | Высота: | Размер: 534 B |
После Ширина: | Высота: | Размер: 318 B |
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011-2015 Twitter, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata></metadata>
|
||||
<defs>
|
||||
<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
|
||||
<font-face units-per-em="1200" ascent="960" descent="-240" />
|
||||
<missing-glyph horiz-adv-x="500" />
|
||||
<glyph />
|
||||
<glyph />
|
||||
<glyph unicode="
" />
|
||||
<glyph unicode=" " />
|
||||
<glyph unicode="*" d="M100 500v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259z" />
|
||||
<glyph unicode="+" d="M0 400v300h400v400h300v-400h400v-300h-400v-400h-300v400h-400z" />
|
||||
<glyph unicode=" " />
|
||||
<glyph unicode=" " horiz-adv-x="652" />
|
||||
<glyph unicode=" " horiz-adv-x="1304" />
|
||||
<glyph unicode=" " horiz-adv-x="652" />
|
||||
<glyph unicode=" " horiz-adv-x="1304" />
|
||||
<glyph unicode=" " horiz-adv-x="434" />
|
||||
<glyph unicode=" " horiz-adv-x="326" />
|
||||
<glyph unicode=" " horiz-adv-x="217" />
|
||||
<glyph unicode=" " horiz-adv-x="217" />
|
||||
<glyph unicode=" " horiz-adv-x="163" />
|
||||
<glyph unicode=" " horiz-adv-x="260" />
|
||||
<glyph unicode=" " horiz-adv-x="72" />
|
||||
<glyph unicode=" " horiz-adv-x="260" />
|
||||
<glyph unicode=" " horiz-adv-x="326" />
|
||||
<glyph unicode="€" d="M100 500l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406l-100 -100 h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217z" />
|
||||
<glyph unicode="−" d="M200 400h900v300h-900v-300z" />
|
||||
<glyph unicode="◼" horiz-adv-x="500" d="M0 0z" />
|
||||
<glyph unicode="☁" d="M-14 494q0 -80 56.5 -137t135.5 -57h750q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5z" />
|
||||
<glyph unicode="✉" d="M0 100l400 400l200 -200l200 200l400 -400h-1200zM0 300v600l300 -300zM0 1100l600 -603l600 603h-1200zM900 600l300 300v-600z" />
|
||||
<glyph unicode="✏" d="M-13 -13l333 112l-223 223zM187 403l214 -214l614 614l-214 214zM887 1103l214 -214l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13z" />
|
||||
<glyph unicode="" d="M0 1200h1200l-500 -550v-550h300v-100h-800v100h300v550z" />
|
||||
<glyph unicode="" d="M14 84q18 -55 86 -75.5t147 5.5q65 21 109 69t44 90v606l600 155v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q18 -55 86 -75.5t147 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7 q-79 -25 -122.5 -82t-25.5 -112z" />
|
||||
<glyph unicode="" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" />
|
||||
<glyph unicode="" d="M100 784q0 64 28 123t73 100.5t104.5 64t119 20.5t120 -38.5t104.5 -104.5q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5 t-94 124.5t-33.5 117.5z" />
|
||||
<glyph unicode="" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1z" />
|
||||
<glyph unicode="" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1zM237 700l196 -142l-73 -226l192 140l195 -141l-74 229l193 140h-235l-77 211l-78 -211h-239z" />
|
||||
<glyph unicode="" d="M0 0v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100l400 -257v-143h-1200z" />
|
||||
<glyph unicode="" d="M0 0v1100h1200v-1100h-1200zM100 100h100v100h-100v-100zM100 300h100v100h-100v-100zM100 500h100v100h-100v-100zM100 700h100v100h-100v-100zM100 900h100v100h-100v-100zM300 100h600v400h-600v-400zM300 600h600v400h-600v-400zM1000 100h100v100h-100v-100z M1000 300h100v100h-100v-100zM1000 500h100v100h-100v-100zM1000 700h100v100h-100v-100zM1000 900h100v100h-100v-100z" />
|
||||
<glyph unicode="" d="M0 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM0 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5zM600 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM600 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5z" />
|
||||
<glyph unicode="" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 450v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5z" />
|
||||
<glyph unicode="" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5 t-14.5 -35.5v-200zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5z" />
|
||||
<glyph unicode="" d="M29 454l419 -420l818 820l-212 212l-607 -607l-206 207z" />
|
||||
<glyph unicode="" d="M106 318l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282l-212 -212l-282 282l-282 -282z" />
|
||||
<glyph unicode="" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233zM300 600v200h100v100h200v-100h100v-200h-100v-100h-200v100h-100z" />
|
||||
<glyph unicode="" d="M23 694q0 200 142 342t342 142t342 -142t142 -342q0 -141 -78 -262l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 601h400v200h-400v-200z" />
|
||||
<glyph unicode="" d="M23 600q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5 zM500 750q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400z" />
|
||||
<glyph unicode="" d="M100 1h200v300h-200v-300zM400 1v500h200v-500h-200zM700 1v800h200v-800h-200zM1000 1v1200h200v-1200h-200z" />
|
||||
<glyph unicode="" d="M26 601q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39l5 -2l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38 l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73zM385 601 q0 88 63 151t152 63t152 -63t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152z" />
|
||||
<glyph unicode="" d="M100 1025v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18zM200 100v800h900v-800q0 -41 -29.5 -71t-70.5 -30h-700q-41 0 -70.5 30 t-29.5 71zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM500 1100h300v100h-300v-100zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" />
|
||||
<glyph unicode="" d="M1 601l656 644l644 -644h-200v-600h-300v400h-300v-400h-300v600h-200z" />
|
||||
<glyph unicode="" d="M100 25v1150q0 11 7 18t18 7h475v-500h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18zM700 800v300l300 -300h-300z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 500v400h100 v-300h200v-100h-300z" />
|
||||
<glyph unicode="" d="M-100 0l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538l-41 400h-242l-40 -400h-539zM488 500h224l-27 300h-170z" />
|
||||
<glyph unicode="" d="M0 0v400h490l-290 300h200v500h300v-500h200l-290 -300h490v-400h-1100zM813 200h175v100h-175v-100z" />
|
||||
<glyph unicode="" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM188 600q0 -170 121 -291t291 -121t291 121t121 291t-121 291t-291 121 t-291 -121t-121 -291zM350 600h150v300h200v-300h150l-250 -300z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM350 600l250 300 l250 -300h-150v-300h-200v300h-150z" />
|
||||
<glyph unicode="" d="M0 25v475l200 700h800l199 -700l1 -475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18zM200 500h200l50 -200h300l50 200h200l-97 500h-606z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 397v401 l297 -200z" />
|
||||
<glyph unicode="" d="M23 600q0 -118 45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123t123 184t45.5 224.5h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123 t-123 -184t-45.5 -224.5z" />
|
||||
<glyph unicode="" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150zM100 0v400h400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122z" />
|
||||
<glyph unicode="" d="M100 0h1100v1200h-1100v-1200zM200 100v900h900v-900h-900zM300 200v100h100v-100h-100zM300 400v100h100v-100h-100zM300 600v100h100v-100h-100zM300 800v100h100v-100h-100zM500 200h500v100h-500v-100zM500 400v100h500v-100h-500zM500 600v100h500v-100h-500z M500 800v100h500v-100h-500z" />
|
||||
<glyph unicode="" d="M0 100v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" />
|
||||
<glyph unicode="" d="M100 0v1100h100v-1100h-100zM300 400q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500z" />
|
||||
<glyph unicode="" d="M0 275q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5 t-49.5 -227v-300zM200 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14zM800 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14z" />
|
||||
<glyph unicode="" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM688 459l141 141l-141 141l71 71l141 -141l141 141l71 -71l-141 -141l141 -141l-71 -71l-141 141l-141 -141z" />
|
||||
<glyph unicode="" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" />
|
||||
<glyph unicode="" d="M0 401v400h300l300 200v-800l-300 200h-300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257zM889 951l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8l81 -66l6 8q142 178 142 405q0 230 -144 408l-6 8z" />
|
||||
<glyph unicode="" d="M0 0h500v500h-200v100h-100v-100h-200v-500zM0 600h100v100h400v100h100v100h-100v300h-500v-600zM100 100v300h300v-300h-300zM100 800v300h300v-300h-300zM200 200v100h100v-100h-100zM200 900h100v100h-100v-100zM500 500v100h300v-300h200v-100h-100v-100h-200v100 h-100v100h100v200h-200zM600 0v100h100v-100h-100zM600 1000h100v-300h200v-300h300v200h-200v100h200v500h-600v-200zM800 800v300h300v-300h-300zM900 0v100h300v-100h-300zM900 900v100h100v-100h-100zM1100 200v100h100v-100h-100z" />
|
||||
<glyph unicode="" d="M0 200h100v1000h-100v-1000zM100 0v100h300v-100h-300zM200 200v1000h100v-1000h-100zM500 0v91h100v-91h-100zM500 200v1000h200v-1000h-200zM700 0v91h100v-91h-100zM800 200v1000h100v-1000h-100zM900 0v91h200v-91h-200zM1000 200v1000h200v-1000h-200z" />
|
||||
<glyph unicode="" d="M0 700l1 475q0 10 7.5 17.5t17.5 7.5h474l700 -700l-500 -500zM148 953q0 -42 29 -71q30 -30 71.5 -30t71.5 30q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71z" />
|
||||
<glyph unicode="" d="M1 700l1 475q0 11 7 18t18 7h474l700 -700l-500 -500zM148 953q0 -42 30 -71q29 -30 71 -30t71 30q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71zM701 1200h100l700 -700l-500 -500l-50 50l450 450z" />
|
||||
<glyph unicode="" d="M100 0v1025l175 175h925v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900z" />
|
||||
<glyph unicode="" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" />
|
||||
<glyph unicode="" d="M0 100v700h200l100 -200h600l100 200h200v-700h-200v200h-800v-200h-200zM253 829l40 -124h592l62 124l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18zM281 24l38 152q2 10 11.5 17t19.5 7h500q10 0 19.5 -7t11.5 -17l38 -152q2 -10 -3.5 -17t-15.5 -7h-600 q-10 0 -15.5 7t-3.5 17z" />
|
||||
<glyph unicode="" d="M0 200q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600z M356 500q0 100 72 172t172 72t172 -72t72 -172t-72 -172t-172 -72t-172 72t-72 172zM494 500q0 -44 31 -75t75 -31t75 31t31 75t-31 75t-75 31t-75 -31t-31 -75zM900 700v100h100v-100h-100z" />
|
||||
<glyph unicode="" d="M53 0h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66zM416 521l178 457l46 -140l116 -317h-340 z" />
|
||||
<glyph unicode="" d="M100 0v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21t-29 14t-49 14.5v71l471 -1q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111 t-162 -38.5h-500zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400zM400 700h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5v-379z" />
|
||||
<glyph unicode="" d="M200 0v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500z" />
|
||||
<glyph unicode="" d="M-75 200h75v800h-75l125 167l125 -167h-75v-800h75l-125 -167zM300 900v300h150h700h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49z " />
|
||||
<glyph unicode="" d="M33 51l167 125v-75h800v75l167 -125l-167 -125v75h-800v-75zM100 901v300h150h700h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50z" />
|
||||
<glyph unicode="" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 350q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM0 650q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 950q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
|
||||
<glyph unicode="" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 650q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM200 350q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM200 950q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
|
||||
<glyph unicode="" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600 q-21 0 -35.5 15t-14.5 35z" />
|
||||
<glyph unicode="" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" />
|
||||
<glyph unicode="" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" />
|
||||
<glyph unicode="" d="M-101 500v100h201v75l166 -125l-166 -125v75h-201zM300 0h100v1100h-100v-1100zM500 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35 v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 650q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100 q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100z" />
|
||||
<glyph unicode="" d="M1 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 650 q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM801 0v1100h100v-1100 h-100zM934 550l167 -125v75h200v100h-200v75z" />
|
||||
<glyph unicode="" d="M0 275v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53zM900 600l300 300v-600z" />
|
||||
<glyph unicode="" d="M0 44v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31zM100 263l247 182l298 -131l-74 156l293 318l236 -288v500h-1000v-737zM208 750q0 56 39 95t95 39t95 -39t39 -95t-39 -95t-95 -39t-95 39t-39 95z " />
|
||||
<glyph unicode="" d="M148 745q0 124 60.5 231.5t165 172t226.5 64.5q123 0 227 -63t164.5 -169.5t60.5 -229.5t-73 -272q-73 -114 -166.5 -237t-150.5 -189l-57 -66q-10 9 -27 26t-66.5 70.5t-96 109t-104 135.5t-100.5 155q-63 139 -63 262zM342 772q0 -107 75.5 -182.5t181.5 -75.5 q107 0 182.5 75.5t75.5 182.5t-75.5 182t-182.5 75t-182 -75.5t-75 -181.5z" />
|
||||
<glyph unicode="" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM173 600q0 -177 125.5 -302t301.5 -125v854q-176 0 -301.5 -125 t-125.5 -302z" />
|
||||
<glyph unicode="" d="M117 406q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 139t-64 210zM243 414q14 -82 59.5 -136 t136.5 -80l16 98q-7 6 -18 17t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156z" />
|
||||
<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125l200 200v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM436 341l161 50l412 412l-114 113l-405 -405zM995 1015l113 -113l113 113l-21 85l-92 28z" />
|
||||
<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5 zM423 524q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5z" />
|
||||
<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q61 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM342 632l283 -284l567 567l-137 137l-430 -431l-146 147z" />
|
||||
<glyph unicode="" d="M0 603l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296l-300 -300v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198z" />
|
||||
<glyph unicode="" d="M200 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
|
||||
<glyph unicode="" d="M0 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
|
||||
<glyph unicode="" d="M136 550l564 550v-487l500 487v-1100l-500 488v-488z" />
|
||||
<glyph unicode="" d="M200 0l900 550l-900 550v-1100z" />
|
||||
<glyph unicode="" d="M200 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800zM600 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
|
||||
<glyph unicode="" d="M200 150q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
|
||||
<glyph unicode="" d="M0 0v1100l500 -487v487l564 -550l-564 -550v488z" />
|
||||
<glyph unicode="" d="M0 0v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488z" />
|
||||
<glyph unicode="" d="M300 0v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438z" />
|
||||
<glyph unicode="" d="M100 250v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5zM100 500h1100l-550 564z" />
|
||||
<glyph unicode="" d="M185 599l592 -592l240 240l-353 353l353 353l-240 240z" />
|
||||
<glyph unicode="" d="M272 194l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1l-592 -591z" />
|
||||
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h200v-200h200v200h200v200h-200v200h-200v-200h-200v-200z" />
|
||||
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h600v200h-600v-200z" />
|
||||
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM246 459l213 -213l141 142l141 -142l213 213l-142 141l142 141l-213 212l-141 -141l-141 142l-212 -213l141 -141 z" />
|
||||
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM270 551l276 -277l411 411l-175 174l-236 -236l-102 102z" />
|
||||
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM364 700h143q4 0 11.5 -1t11 -1t6.5 3t3 9t1 11t3.5 8.5t3.5 6t5.5 4t6.5 2.5t9 1.5t9 0.5h11.5h12.5 q19 0 30 -10t11 -26q0 -22 -4 -28t-27 -22q-5 -1 -12.5 -3t-27 -13.5t-34 -27t-26.5 -46t-11 -68.5h200q5 3 14 8t31.5 25.5t39.5 45.5t31 69t14 94q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3q-50 0 -90.5 -12t-75 -38.5t-53.5 -74.5t-19 -114zM500 300h200v100h-200 v-100z" />
|
||||
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM400 300h400v100h-100v300h-300v-100h100v-200h-100v-100zM500 800h200v100h-200v-100z" />
|
||||
<glyph unicode="" d="M0 500v200h195q31 125 98.5 199.5t206.5 100.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h203v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194zM290 500q24 -73 79.5 -127.5t130.5 -78.5v206h200v-206 q149 48 201 206h-201v200h200q-25 74 -75.5 127t-124.5 77v-204h-200v203q-75 -23 -130 -77t-79 -126h209v-200h-210z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM356 465l135 135 l-135 135l109 109l135 -135l135 135l109 -109l-135 -135l135 -135l-109 -109l-135 135l-135 -135z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM322 537l141 141 l87 -87l204 205l142 -142l-346 -345z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -115 62 -215l568 567q-100 62 -216 62q-171 0 -292.5 -121.5t-121.5 -292.5zM391 245q97 -59 209 -59q171 0 292.5 121.5t121.5 292.5 q0 112 -59 209z" />
|
||||
<glyph unicode="" d="M0 547l600 453v-300h600v-300h-600v-301z" />
|
||||
<glyph unicode="" d="M0 400v300h600v300l600 -453l-600 -448v301h-600z" />
|
||||
<glyph unicode="" d="M204 600l450 600l444 -600h-298v-600h-300v600h-296z" />
|
||||
<glyph unicode="" d="M104 600h296v600h300v-600h298l-449 -600z" />
|
||||
<glyph unicode="" d="M0 200q6 132 41 238.5t103.5 193t184 138t271.5 59.5v271l600 -453l-600 -448v301q-95 -2 -183 -20t-170 -52t-147 -92.5t-100 -135.5z" />
|
||||
<glyph unicode="" d="M0 0v400l129 -129l294 294l142 -142l-294 -294l129 -129h-400zM635 777l142 -142l294 294l129 -129v400h-400l129 -129z" />
|
||||
<glyph unicode="" d="M34 176l295 295l-129 129h400v-400l-129 130l-295 -295zM600 600v400l129 -129l295 295l142 -141l-295 -295l129 -130h-400z" />
|
||||
<glyph unicode="" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM456 851l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5 t21.5 34.5l58 302q4 20 -8 34.5t-32 14.5h-207q-21 0 -33 -14.5t-8 -34.5zM500 300h200v100h-200v-100z" />
|
||||
<glyph unicode="" d="M0 800h100v-200h400v300h200v-300h400v200h100v100h-111q1 1 1 6.5t-1.5 15t-3.5 17.5l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6 h-111v-100zM100 0h400v400h-400v-400zM200 900q-3 0 14 48t36 96l18 47l213 -191h-281zM700 0v400h400v-400h-400zM731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269z" />
|
||||
<glyph unicode="" d="M0 -22v143l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55z M238.5 300.5q19.5 -6.5 86.5 76.5q55 66 367 234q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5z" />
|
||||
<glyph unicode="" d="M111 408q0 -33 5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-5 -33 -20.5 -69.5t-37.5 -80.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5t30.5 2.5 t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5q2 -12 8 -41.5t8 -43t6 -39.5 t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85z" />
|
||||
<glyph unicode="" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30l26 -40l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5 t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30zM120 600q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5t123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54 q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l105 105q-37 24 -75 72t-57 84l-20 36z" />
|
||||
<glyph unicode="" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43zM120 600q210 -282 393 -336l37 141q-107 18 -178.5 101.5t-71.5 193.5 q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l47 47l23 87q-30 28 -59 69t-44 68l-14 26zM780 161l38 145q22 15 44.5 34t46 44t40.5 44t41 50.5t33.5 43.5t33 44t24.5 34q-97 127 -140 175l39 146q67 -54 131.5 -125.5t87.5 -103.5t36 -52l26 -40l-26 -40 q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5t-124 -100t-146.5 -79z" />
|
||||
<glyph unicode="" d="M-97.5 34q13.5 -34 50.5 -34h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 34 -48 36.5t-48 -29.5l-642 -1066q-21 -32 -7.5 -66zM155 200l445 723l445 -723h-345v100h-200v-100h-345zM500 600l100 -300l100 300v100h-200v-100z" />
|
||||
<glyph unicode="" d="M100 262v41q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -20 -13 -28.5t-32 0.5l-94 78h-222l-94 -78q-19 -9 -32 -0.5t-13 28.5 v64q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5z" />
|
||||
<glyph unicode="" d="M0 50q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100v-750zM0 900h1100v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 100v100h100v-100h-100zM100 300v100h100v-100h-100z M100 500v100h100v-100h-100zM300 100v100h100v-100h-100zM300 300v100h100v-100h-100zM300 500v100h100v-100h-100zM500 100v100h100v-100h-100zM500 300v100h100v-100h-100zM500 500v100h100v-100h-100zM700 100v100h100v-100h-100zM700 300v100h100v-100h-100zM700 500 v100h100v-100h-100zM900 100v100h100v-100h-100zM900 300v100h100v-100h-100zM900 500v100h100v-100h-100z" />
|
||||
<glyph unicode="" d="M0 200v200h259l600 600h241v198l300 -295l-300 -300v197h-159l-600 -600h-341zM0 800h259l122 -122l141 142l-181 180h-341v-200zM678 381l141 142l122 -123h159v198l300 -295l-300 -300v197h-241z" />
|
||||
<glyph unicode="" d="M0 400v600q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5z" />
|
||||
<glyph unicode="" d="M100 600v200h300v-250q0 -113 6 -145q17 -92 102 -117q39 -11 92 -11q37 0 66.5 5.5t50 15.5t36 24t24 31.5t14 37.5t7 42t2.5 45t0 47v25v250h300v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5t-89 96.5 t-58 109.5t-31.5 116t-15 104t-3 83zM100 900v300h300v-300h-300zM800 900v300h300v-300h-300z" />
|
||||
<glyph unicode="" d="M-30 411l227 -227l352 353l353 -353l226 227l-578 579z" />
|
||||
<glyph unicode="" d="M70 797l580 -579l578 579l-226 227l-353 -353l-352 353z" />
|
||||
<glyph unicode="" d="M-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196zM402 1000l215 -200h381v-400h-198l299 -283l299 283h-200v600h-796z" />
|
||||
<glyph unicode="" d="M18 939q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-300v-50q0 -20 -14.5 -35t-35.5 -15 t-35.5 15t-14.5 35v50h-50q-21 0 -35.5 15t-14.5 35q0 21 14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43z" />
|
||||
<glyph unicode="" d="M0 0v800h1200v-800h-1200zM0 900v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-100h-1200z" />
|
||||
<glyph unicode="" d="M1 0l300 700h1200l-300 -700h-1200zM1 400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000z" />
|
||||
<glyph unicode="" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" />
|
||||
<glyph unicode="" d="M0 600l300 298v-198h600v198l300 -298l-300 -297v197h-600v-197z" />
|
||||
<glyph unicode="" d="M0 100v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM800 100h100v100h-100v-100z M1000 100h100v100h-100v-100z" />
|
||||
<glyph unicode="" d="M-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5zM100 500v250v8v8v7t0.5 7t1.5 5.5t2 5t3 4t4.5 3.5t6 1.5t7.5 0.5h200l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35 q-55 337 -55 351zM1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35z" />
|
||||
<glyph unicode="" d="M74 350q0 21 13.5 35.5t33.5 14.5h18l117 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94q20 0 29 -10.5t3 -29.5q-18 -36 -18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-8 -3 -23 -8.5 t-65 -20t-103 -25t-132.5 -19.5t-158.5 -9q-125 0 -245.5 20.5t-178.5 40.5l-58 20q-18 7 -31 27.5t-13 40.5zM497 110q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6t-103 6z" />
|
||||
<glyph unicode="" d="M21 445l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180l-155 180l-45 -233l-224 78l78 -225l-233 -44l179 -156z" />
|
||||
<glyph unicode="" d="M0 200h200v600h-200v-600zM300 275q0 -75 100 -75h61q124 -100 139 -100h250q46 0 83 57l238 344q29 31 29 74v100q0 44 -30.5 84.5t-69.5 40.5h-328q28 118 28 125v150q0 44 -30.5 84.5t-69.5 40.5h-50q-27 0 -51 -20t-38 -48l-96 -198l-145 -196q-20 -26 -20 -63v-400z M400 300v375l150 213l100 212h50v-175l-50 -225h450v-125l-250 -375h-214l-136 100h-100z" />
|
||||
<glyph unicode="" d="M0 400v600h200v-600h-200zM300 525v400q0 75 100 75h61q124 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5h-50q-27 0 -51 20t-38 48l-96 198l-145 196 q-20 26 -20 63zM400 525l150 -212l100 -213h50v175l-50 225h450v125l-250 375h-214l-136 -100h-100v-375z" />
|
||||
<glyph unicode="" d="M8 200v600h200v-600h-200zM308 275v525q0 17 14 35.5t28 28.5l14 9l362 230q14 6 25 6q17 0 29 -12l109 -112q14 -14 14 -34q0 -18 -11 -32l-85 -121h302q85 0 138.5 -38t53.5 -110t-54.5 -111t-138.5 -39h-107l-130 -339q-7 -22 -20.5 -41.5t-28.5 -19.5h-341 q-7 0 -90 81t-83 94zM408 289l100 -89h293l131 339q6 21 19.5 41t28.5 20h203q16 0 25 15t9 36q0 20 -9 34.5t-25 14.5h-457h-6.5h-7.5t-6.5 0.5t-6 1t-5 1.5t-5.5 2.5t-4 4t-4 5.5q-5 12 -5 20q0 14 10 27l147 183l-86 83l-339 -236v-503z" />
|
||||
<glyph unicode="" d="M-101 651q0 72 54 110t139 38l302 -1l-85 121q-11 16 -11 32q0 21 14 34l109 113q13 12 29 12q11 0 25 -6l365 -230q7 -4 17 -10.5t26.5 -26t16.5 -36.5v-526q0 -13 -86 -93.5t-94 -80.5h-341q-16 0 -29.5 20t-19.5 41l-130 339h-107q-84 0 -139 39t-55 111zM-1 601h222 q15 0 28.5 -20.5t19.5 -40.5l131 -339h293l107 89v502l-343 237l-87 -83l145 -184q10 -11 10 -26q0 -11 -5 -20q-1 -3 -3.5 -5.5l-4 -4t-5 -2.5t-5.5 -1.5t-6.5 -1t-6.5 -0.5h-7.5h-6.5h-476v-100zM1000 201v600h200v-600h-200z" />
|
||||
<glyph unicode="" d="M97 719l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53zM172 739l83 86l183 -146 q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6.5v7.5v6.5v456q0 22 25 31t50 -0.5t25 -30.5v-202q0 -16 20 -29.5t41 -19.5l339 -130v-294l-89 -100h-503zM400 0v200h600v-200h-600z" />
|
||||
<glyph unicode="" d="M2 585q-16 -31 6 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85q0 -51 -0.5 -153.5t-0.5 -148.5q0 -84 38.5 -138t110.5 -54t111 55t39 139v106l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15zM77 565l236 339h503 l89 -100v-294l-340 -130q-20 -6 -40 -20t-20 -29v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146zM305 1104v200h600v-200h-600z" />
|
||||
<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM298 701l2 -201h300l-2 -194l402 294l-402 298v-197h-300z" />
|
||||
<glyph unicode="" d="M0 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t231.5 47.5q122 0 232.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-218 -217.5t-300 -80t-299.5 80t-217.5 217.5t-80 299.5zM200 600l402 -294l-2 194h300l2 201h-300v197z" />
|
||||
<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600h200v-300h200v300h200l-300 400z" />
|
||||
<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600l300 -400l300 400h-200v300h-200v-300h-200z" />
|
||||
<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM254 780q-8 -33 5.5 -92.5t7.5 -87.5q0 -9 17 -44t16 -60 q12 0 23 -5.5t23 -15t20 -13.5q24 -12 108 -42q22 -8 53 -31.5t59.5 -38.5t57.5 -11q8 -18 -15 -55t-20 -57q42 -71 87 -80q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q104 -3 221 112q30 29 47 47t34.5 49t20.5 62q-14 9 -37 9.5t-36 7.5q-14 7 -49 15t-52 19q-9 0 -39.5 -0.5 t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5t5.5 57.5 q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5t34 21.5q-6 18 10 37q8 0 23.5 -1.5t24.5 -1.5t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 39 2 44q31 -13 58 -14.5t39 3.5l11 4q7 36 -16.5 53.5t-64.5 28.5t-56 23q-19 -3 -37 0 q-15 -12 -36.5 -21t-34.5 -12t-44 -8t-39 -6q-15 -3 -45.5 0.5t-45.5 -2.5q-21 -7 -52 -26.5t-34 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -90.5t-29.5 -79.5zM518 916q3 12 16 30t16 25q10 -10 18.5 -10t14 6t14.5 14.5t16 12.5q0 -24 17 -66.5t17 -43.5 q-9 2 -31 5t-36 5t-32 8t-30 14zM692 1003h1h-1z" />
|
||||
<glyph unicode="" d="M0 164.5q0 21.5 15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138l145 -232l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1220" d="M0 196v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 596v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5zM0 996v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM600 596h500v100h-500v-100zM800 196h300v100h-300v-100zM900 996h200v100h-200v-100z" />
|
||||
<glyph unicode="" d="M100 1100v100h1000v-100h-1000zM150 1000h900l-350 -500v-300l-200 -200v500z" />
|
||||
<glyph unicode="" d="M0 200v200h1200v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500z M500 1000h200v100h-200v-100z" />
|
||||
<glyph unicode="" d="M0 0v400l129 -129l200 200l142 -142l-200 -200l129 -129h-400zM0 800l129 129l200 -200l142 142l-200 200l129 129h-400v-400zM729 329l142 142l200 -200l129 129v-400h-400l129 129zM729 871l200 200l-129 129h400v-400l-129 129l-200 -200z" />
|
||||
<glyph unicode="" d="M0 596q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 596q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM291 655 q0 23 15.5 38.5t38.5 15.5t39 -16t16 -38q0 -23 -16 -39t-39 -16q-22 0 -38 16t-16 39zM400 850q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5zM514 609q0 32 20.5 56.5t51.5 29.5l122 126l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5 q22 0 38 -16t16 -39t-16 -39t-38 -16q-14 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-61.5 25.5t-25.5 61.5zM800 655q0 22 16 38t39 16t38.5 -15.5t15.5 -38.5t-16 -39t-38 -16q-23 0 -39 16t-16 39z" />
|
||||
<glyph unicode="" d="M-40 375q-13 -95 35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -78.5 -16.5t-67.5 -51.5l-389 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23 q38 0 53 -36q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256l7 -7l69 -60 l517 511q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-40 -24 -111 -95l-512 -512q-68 -68 -81 -163z" />
|
||||
<glyph unicode="" d="M80 784q0 131 98.5 229.5t230.5 98.5q143 0 241 -129q103 129 246 129q129 0 226 -98.5t97 -229.5q0 -46 -17.5 -91t-61 -99t-77 -89.5t-104.5 -105.5q-197 -191 -293 -322l-17 -23l-16 23q-43 58 -100 122.5t-92 99.5t-101 100q-71 70 -104.5 105.5t-77 89.5t-61 99 t-17.5 91zM250 784q0 -27 30.5 -70t61.5 -75.5t95 -94.5l22 -22q93 -90 190 -201q82 92 195 203l12 12q64 62 97.5 97t64.5 79t31 72q0 71 -48 119.5t-105 48.5q-74 0 -132 -83l-118 -171l-114 174q-51 80 -123 80q-60 0 -109.5 -49.5t-49.5 -118.5z" />
|
||||
<glyph unicode="" d="M57 353q0 -95 66 -159l141 -142q68 -66 159 -66q93 0 159 66l283 283q66 66 66 159t-66 159l-141 141q-8 9 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159zM269 706q0 -93 66 -159l141 -141q7 -7 19 -17l105 105 l-212 212l389 389l247 -247l-95 -96l18 -17q47 -49 77 -100l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159z" />
|
||||
<glyph unicode="" d="M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM300 300h600v700h-600v-700zM496 150q0 -43 30.5 -73.5t73.5 -30.5t73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5 t-73.5 -30.5t-30.5 -73.5z" />
|
||||
<glyph unicode="" d="M0 0l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207z" />
|
||||
<glyph unicode="" d="M295 433h139q5 -77 48.5 -126.5t117.5 -64.5v335q-6 1 -15.5 4t-11.5 3q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5 v-307l64 -14q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5zM466 889q0 -29 8 -51t16.5 -34t29.5 -22.5t31 -13.5t38 -10q7 -2 11 -3v274q-61 -8 -97.5 -37.5t-36.5 -102.5 zM700 237q170 18 170 151q0 64 -44 99.5t-126 60.5v-311z" />
|
||||
<glyph unicode="" d="M100 600v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -28 16.5 -69.5t28 -62.5t41.5 -72h241v-100h-197q8 -50 -2.5 -115 t-31.5 -94q-41 -59 -99 -113q35 11 84 18t70 7q33 1 103 -16t103 -17q76 0 136 30l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5 t-30 142.5h-221z" />
|
||||
<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM602 900l298 300l298 -300h-198v-900h-200v900h-198z" />
|
||||
<glyph unicode="" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v200h100v-100h200v-100h-300zM700 400v100h300v-200h-99v-100h-100v100h99v100h-200zM700 700v500h300v-500h-100v100h-100v-100h-100zM801 900h100v200h-100v-200z" />
|
||||
<glyph unicode="" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v500h300v-500h-100v100h-100v-100h-100zM700 700v200h100v-100h200v-100h-300zM700 1100v100h300v-200h-99v-100h-100v100h99v100h-200zM801 200h100v200h-100v-200z" />
|
||||
<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 100v400h300v-500h-100v100h-200zM800 1100v100h200v-500h-100v400h-100zM901 200h100v200h-100v-200z" />
|
||||
<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 400v100h200v-500h-100v400h-100zM800 800v400h300v-500h-100v100h-200zM901 900h100v200h-100v-200z" />
|
||||
<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h500v-200h-500zM700 400v200h400v-200h-400zM700 700v200h300v-200h-300zM700 1000v200h200v-200h-200z" />
|
||||
<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h200v-200h-200zM700 400v200h300v-200h-300zM700 700v200h400v-200h-400zM700 1000v200h500v-200h-500z" />
|
||||
<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500z" />
|
||||
<glyph unicode="" d="M0 400v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-163 0 -281.5 117.5t-118.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM400 300l333 250l-333 250v-500z" />
|
||||
<glyph unicode="" d="M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 700l250 -333l250 333h-500z" />
|
||||
<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 400h500l-250 333z" />
|
||||
<glyph unicode="" d="M0 400v300h300v200l400 -350l-400 -350v200h-300zM500 0v200h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-500v200h400q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-400z" />
|
||||
<glyph unicode="" d="M217 519q8 -19 31 -19h302q-155 -438 -160 -458q-5 -21 4 -32l9 -8h9q14 0 26 15q11 13 274.5 321.5t264.5 308.5q14 19 5 36q-8 17 -31 17l-301 -1q1 4 78 219.5t79 227.5q2 15 -5 27l-9 9h-9q-15 0 -25 -16q-4 -6 -98 -111.5t-228.5 -257t-209.5 -237.5q-16 -19 -6 -41 z" />
|
||||
<glyph unicode="" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q47 0 100 15v185h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h500v185q-14 4 -114 7.5t-193 5.5l-93 2q-165 0 -282.5 -117.5t-117.5 -282.5v-300zM600 400v300h300v200l400 -350l-400 -350v200h-300z " />
|
||||
<glyph unicode="" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q163 0 281.5 117.5t118.5 282.5v98l-78 73l-122 -123v-148q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h156l118 122l-74 78h-100q-165 0 -282.5 -117.5t-117.5 -282.5 v-300zM496 709l353 342l-149 149h500v-500l-149 149l-342 -353z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM406 600 q0 80 57 137t137 57t137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137z" />
|
||||
<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 800l445 -500l450 500h-295v400h-300v-400h-300zM900 150h100v50h-100v-50z" />
|
||||
<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 700h300v-300h300v300h295l-445 500zM900 150h100v50h-100v-50z" />
|
||||
<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 705l305 -305l596 596l-154 155l-442 -442l-150 151zM900 150h100v50h-100v-50z" />
|
||||
<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 988l97 -98l212 213l-97 97zM200 400l697 1l3 699l-250 -239l-149 149l-212 -212l149 -149zM900 150h100v50h-100v-50z" />
|
||||
<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM200 612l212 -212l98 97l-213 212zM300 1200l239 -250l-149 -149l212 -212l149 148l249 -237l-1 697zM900 150h100v50h-100v-50z" />
|
||||
<glyph unicode="" d="M23 415l1177 784v-1079l-475 272l-310 -393v416h-392zM494 210l672 938l-672 -712v-226z" />
|
||||
<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-850q0 -21 -15 -35.5t-35 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200z" />
|
||||
<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-218l-276 -275l-120 120l-126 -127h-378v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM581 306l123 123l120 -120l353 352l123 -123l-475 -476zM600 1000h100v200h-100v-200z" />
|
||||
<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-269l-103 -103l-170 170l-298 -298h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200zM700 133l170 170l-170 170l127 127l170 -170l170 170l127 -128l-170 -169l170 -170 l-127 -127l-170 170l-170 -170z" />
|
||||
<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-300h-400v-200h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300l300 -300l300 300h-200v300h-200v-300h-200zM600 1000v200h100v-200h-100z" />
|
||||
<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-402l-200 200l-298 -298h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300h200v-300h200v300h200l-300 300zM600 1000v200h100v-200h-100z" />
|
||||
<glyph unicode="" d="M0 250q0 -21 14.5 -35.5t35.5 -14.5h1100q21 0 35.5 14.5t14.5 35.5v550h-1200v-550zM0 900h1200v150q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 300v200h400v-200h-400z" />
|
||||
<glyph unicode="" d="M0 400l300 298v-198h400v-200h-400v-198zM100 800v200h100v-200h-100zM300 800v200h100v-200h-100zM500 800v200h400v198l300 -298l-300 -298v198h-400zM800 300v200h100v-200h-100zM1000 300h100v200h-100v-200z" />
|
||||
<glyph unicode="" d="M100 700v400l50 100l50 -100v-300h100v300l50 100l50 -100v-300h100v300l50 100l50 -100v-400l-100 -203v-447q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447zM800 597q0 -29 10.5 -55.5t25 -43t29 -28.5t25.5 -18l10 -5v-397q0 -21 14.5 -35.5 t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v1106q0 31 -18 40.5t-44 -7.5l-276 -116q-25 -17 -43.5 -51.5t-18.5 -65.5v-359z" />
|
||||
<glyph unicode="" d="M100 0h400v56q-75 0 -87.5 6t-12.5 44v394h500v-394q0 -38 -12.5 -44t-87.5 -6v-56h400v56q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5v888q0 22 25 34.5t50 13.5l25 2v56h-400v-56q75 0 87.5 -6t12.5 -44v-394h-500v394q0 38 12.5 44t87.5 6v56h-400v-56q4 0 11 -0.5 t24 -3t30 -7t24 -15t11 -24.5v-888q0 -22 -25 -34.5t-50 -13.5l-25 -2v-56z" />
|
||||
<glyph unicode="" d="M0 300q0 -41 29.5 -70.5t70.5 -29.5h300q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-300q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM100 100h400l200 200h105l295 98v-298h-425l-100 -100h-375zM100 300v200h300v-200h-300zM100 600v200h300v-200h-300z M100 1000h400l200 -200v-98l295 98h105v200h-425l-100 100h-375zM700 402v163l400 133v-163z" />
|
||||
<glyph unicode="" d="M16.5 974.5q0.5 -21.5 16 -90t46.5 -140t104 -177.5t175 -208q103 -103 207.5 -176t180 -103.5t137 -47t92.5 -16.5l31 1l163 162q17 18 13.5 41t-22.5 37l-192 136q-19 14 -45 12t-42 -19l-118 -118q-142 101 -268 227t-227 268l118 118q17 17 20 41.5t-11 44.5 l-139 194q-14 19 -36.5 22t-40.5 -14l-162 -162q-1 -11 -0.5 -32.5z" />
|
||||
<glyph unicode="" d="M0 50v212q0 20 10.5 45.5t24.5 39.5l365 303v50q0 4 1 10.5t12 22.5t30 28.5t60 23t97 10.5t97 -10t60 -23.5t30 -27.5t12 -24l1 -10v-50l365 -303q14 -14 24.5 -39.5t10.5 -45.5v-212q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-20 0 -35 14.5t-15 35.5zM0 712 q0 -21 14.5 -33.5t34.5 -8.5l202 33q20 4 34.5 21t14.5 38v146q141 24 300 24t300 -24v-146q0 -21 14.5 -38t34.5 -21l202 -33q20 -4 34.5 8.5t14.5 33.5v200q-6 8 -19 20.5t-63 45t-112 57t-171 45t-235 20.5q-92 0 -175 -10.5t-141.5 -27t-108.5 -36.5t-81.5 -40 t-53.5 -36.5t-31 -27.5l-9 -10v-200z" />
|
||||
<glyph unicode="" d="M100 0v100h1100v-100h-1100zM175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250z" />
|
||||
<glyph unicode="" d="M100 0h300v400q0 41 -29.5 70.5t-70.5 29.5h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-400zM500 0v1000q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-1000h-300zM900 0v700q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-700h-300z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h100v200h100v-200h100v500h-100v-200h-100v200h-100v-500zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v100h-200v300h200v100h-300v-500zM600 300h300v100h-200v300h200v100h-300v-500z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 550l300 -150v300zM600 400l300 150l-300 150v-300z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300v500h700v-500h-700zM300 400h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130v-300zM575 549 q0 -65 27 -107t68 -42h130v300h-130q-38 0 -66.5 -43t-28.5 -108z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v400h-200v100h-100v-500zM301 400v200h100v-200h-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 700v100h300v-300h-99v-100h-100v100h99v200h-200zM201 300v100h100v-100h-100zM601 300v100h100v-100h-100z M700 700v100h200v-500h-100v400h-100z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 500v200 l100 100h300v-100h-300v-200h300v-100h-300z" />
|
||||
<glyph unicode="" d="M0 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 400v400h300 l100 -100v-100h-100v100h-200v-100h200v-100h-200v-100h-100zM700 400v100h100v-100h-100z" />
|
||||
<glyph unicode="" d="M-14 494q0 -80 56.5 -137t135.5 -57h222v300h400v-300h128q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200h200v300h200v-300h200 l-300 -300z" />
|
||||
<glyph unicode="" d="M-14 494q0 -80 56.5 -137t135.5 -57h8l414 414l403 -403q94 26 154.5 104.5t60.5 178.5q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200l300 300 l300 -300h-200v-300h-200v300h-200z" />
|
||||
<glyph unicode="" d="M100 200h400v-155l-75 -45h350l-75 45v155h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170z" />
|
||||
<glyph unicode="" d="M121 700q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350l-75 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5 t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -11.5t1 -11.5q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5z" />
|
||||
</font>
|
||||
</defs></svg>
|
После Ширина: | Высота: | Размер: 62 KiB |
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011-2015 Twitter, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
После Ширина: | Высота: | Размер: 37 KiB |
После Ширина: | Высота: | Размер: 8.6 KiB |
После Ширина: | Высота: | Размер: 12 KiB |
После Ширина: | Высота: | Размер: 16 KiB |
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011-2015 Twitter, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Thomas Park
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,372 @@
|
|||
Copyright (c) 2014 Alexander Farkas (aFarkas).
|
||||
|
||||
This software is licensed under a dual license system (MIT or GPL version 2).
|
||||
This means you are free to choose with which of both licenses (MIT or
|
||||
GPL version 2) you want to use this library.
|
||||
|
||||
The license texts of the MIT license and the GPL version 2 are as follows:
|
||||
|
||||
|
||||
## MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
|
||||
## GNU GENERAL PUBLIC LICENSE Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Library General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<Html5shiv, The HTML5 Shiv enables use of HTML5 sectioning elements in
|
||||
legacy Internet Explorer and provides basic HTML5 styling for Internet Explorer 6-9,
|
||||
Safari 4.x (and iPhone 3.x), and Firefox 3.x.>
|
||||
Copyright (C) 2014 Alexander Farkas (aFarkas)
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) 2014 Alexander Farkas (aFarkas)
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General
|
||||
Public License instead of this License.
|
|
@ -0,0 +1,37 @@
|
|||
Copyright jQuery Foundation and other contributors, https://jquery.org/
|
||||
|
||||
This software consists of voluntary contributions made by many
|
||||
individuals. For exact contribution history, see the revision history
|
||||
available at https://github.com/jquery/jquery
|
||||
|
||||
The following license applies to all parts of this software except as
|
||||
documented below:
|
||||
|
||||
====
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
====
|
||||
|
||||
All files located in the node_modules and external directories are
|
||||
externally maintained libraries used by this software which have their
|
||||
own licenses; we recommend you read them, as their terms may differ from
|
||||
the terms above.
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
Copyright (c) 2008-2015 Ryan McGeary
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
Copyright 2013 jQuery Foundation and other contributors
|
||||
http://jquery.com/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
HTML5 Shiv v3.7.0 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
|
||||
*/
|
||||
(function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag();
|
||||
a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/[\w\-]+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x<style>article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}</style>";
|
||||
c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML="<xyz></xyz>";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode||
|
||||
"undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:"3.7.0",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f);
|
||||
if(g)return a.createDocumentFragment();for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d<h;d++)c.createElement(e[d]);return c}};l.html5=e;q(f)})(this,document);
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright (c) 2008 Greg Weber greg at gregweber.info
|
||||
* Dual licensed under the MIT and GPLv2 licenses just as jQuery is:
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* Multi-columns fork by natinusala
|
||||
*
|
||||
* documentation at http://gregweber.info/projects/uitablefilter
|
||||
* https://github.com/natinusala/jquery-uitablefilter
|
||||
*
|
||||
* allows table rows to be filtered (made invisible)
|
||||
* <code>
|
||||
* t = $('table')
|
||||
* $.uiTableFilter( t, phrase )
|
||||
* </code>
|
||||
* arguments:
|
||||
* jQuery object containing table rows
|
||||
* phrase to search for
|
||||
* optional arguments:
|
||||
* array of columns to limit search too (the column title in the table header)
|
||||
* ifHidden - callback to execute if one or more elements was hidden
|
||||
* tdElem - specific element within <td> to be considered for searching or to limit search to,
|
||||
* default:whole <td>. useful if <td> has more than one elements inside but want to
|
||||
* limit search within only some of elements or only visible elements. eg tdElem can be "td span"
|
||||
*/
|
||||
(function($) {
|
||||
$.uiTableFilter = function(jq, phrase, column, ifHidden, tdElem){
|
||||
if(!tdElem) tdElem = "td";
|
||||
var new_hidden = false;
|
||||
if( this.last_phrase === phrase ) return false;
|
||||
|
||||
var phrase_length = phrase.length;
|
||||
var words = phrase.toLowerCase().split(" ");
|
||||
|
||||
// these function pointers may change
|
||||
var matches = function(elem) { elem.show() }
|
||||
var noMatch = function(elem) { elem.hide(); new_hidden = true }
|
||||
var getText = function(elem) { return elem.text() }
|
||||
|
||||
if( column )
|
||||
{
|
||||
if (!$.isArray(column))
|
||||
{
|
||||
column = new Array(column);
|
||||
}
|
||||
|
||||
var index = new Array();
|
||||
|
||||
jq.find("thead > tr:last > th").each(function(i)
|
||||
{
|
||||
for (var j = 0; j < column.length; j++)
|
||||
{
|
||||
if ($.trim($(this).text()) == column[j])
|
||||
{
|
||||
index[j] = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
getText = function(elem) {
|
||||
var selector = "";
|
||||
for (var i = 0; i < index.length; i++)
|
||||
{
|
||||
if (i != 0) {selector += ",";}
|
||||
selector += tdElem + ":eq(" + index[i] + ")";
|
||||
}
|
||||
return $(elem.find((selector))).text();
|
||||
}
|
||||
}
|
||||
|
||||
// if added one letter to last time,
|
||||
// just check newest word and only need to hide
|
||||
if( (words.size > 1) && (phrase.substr(0, phrase_length - 1) ===
|
||||
this.last_phrase) ) {
|
||||
|
||||
if( phrase[-1] === " " )
|
||||
{ this.last_phrase = phrase; return false; }
|
||||
|
||||
var words = words[-1]; // just search for the newest word
|
||||
|
||||
// only hide visible rows
|
||||
matches = function(elem) {;}
|
||||
var elems = jq.find("tbody:first > tr:visible")
|
||||
}
|
||||
else {
|
||||
new_hidden = true;
|
||||
var elems = jq.find("tbody:first > tr")
|
||||
}
|
||||
|
||||
elems.each(function(){
|
||||
var elem = $(this);
|
||||
$.uiTableFilter.has_words( getText(elem), words, false ) ?
|
||||
matches(elem) : noMatch(elem);
|
||||
});
|
||||
|
||||
last_phrase = phrase;
|
||||
if( ifHidden && new_hidden ) ifHidden();
|
||||
return jq;
|
||||
};
|
||||
|
||||
// caching for speedup
|
||||
$.uiTableFilter.last_phrase = ""
|
||||
|
||||
// not jQuery dependent
|
||||
// "" [""] -> Boolean
|
||||
// "" [""] Boolean -> Boolean
|
||||
$.uiTableFilter.has_words = function( str, words, caseSensitive )
|
||||
{
|
||||
var text = caseSensitive ? str : str.toLowerCase();
|
||||
for (var i=0; i < words.length; i++) {
|
||||
if (text.indexOf(words[i]) === -1) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}) (jQuery);
|
|
@ -0,0 +1,214 @@
|
|||
/**
|
||||
* Timeago is a jQuery plugin that makes it easy to support automatically
|
||||
* updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
|
||||
*
|
||||
* @name timeago
|
||||
* @version 1.4.1
|
||||
* @requires jQuery v1.2.3+
|
||||
* @author Ryan McGeary
|
||||
* @license MIT License - http://www.opensource.org/licenses/mit-license.php
|
||||
*
|
||||
* For usage and examples, visit:
|
||||
* http://timeago.yarp.com/
|
||||
*
|
||||
* Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
|
||||
*/
|
||||
|
||||
(function (factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['jquery'], factory);
|
||||
} else {
|
||||
// Browser globals
|
||||
factory(jQuery);
|
||||
}
|
||||
}(function ($) {
|
||||
$.timeago = function(timestamp) {
|
||||
if (timestamp instanceof Date) {
|
||||
return inWords(timestamp);
|
||||
} else if (typeof timestamp === "string") {
|
||||
return inWords($.timeago.parse(timestamp));
|
||||
} else if (typeof timestamp === "number") {
|
||||
return inWords(new Date(timestamp));
|
||||
} else {
|
||||
return inWords($.timeago.datetime(timestamp));
|
||||
}
|
||||
};
|
||||
var $t = $.timeago;
|
||||
|
||||
$.extend($.timeago, {
|
||||
settings: {
|
||||
refreshMillis: 60000,
|
||||
allowPast: true,
|
||||
allowFuture: false,
|
||||
localeTitle: false,
|
||||
cutoff: 0,
|
||||
strings: {
|
||||
prefixAgo: null,
|
||||
prefixFromNow: null,
|
||||
suffixAgo: "ago",
|
||||
suffixFromNow: "from now",
|
||||
inPast: 'any moment now',
|
||||
seconds: "less than a minute",
|
||||
minute: "about a minute",
|
||||
minutes: "%d minutes",
|
||||
hour: "about an hour",
|
||||
hours: "about %d hours",
|
||||
day: "a day",
|
||||
days: "%d days",
|
||||
month: "about a month",
|
||||
months: "%d months",
|
||||
year: "about a year",
|
||||
years: "%d years",
|
||||
wordSeparator: " ",
|
||||
numbers: []
|
||||
}
|
||||
},
|
||||
|
||||
inWords: function(distanceMillis) {
|
||||
if(!this.settings.allowPast && ! this.settings.allowFuture) {
|
||||
throw 'timeago allowPast and allowFuture settings can not both be set to false.';
|
||||
}
|
||||
|
||||
var $l = this.settings.strings;
|
||||
var prefix = $l.prefixAgo;
|
||||
var suffix = $l.suffixAgo;
|
||||
if (this.settings.allowFuture) {
|
||||
if (distanceMillis < 0) {
|
||||
prefix = $l.prefixFromNow;
|
||||
suffix = $l.suffixFromNow;
|
||||
}
|
||||
}
|
||||
|
||||
if(!this.settings.allowPast && distanceMillis >= 0) {
|
||||
return this.settings.strings.inPast;
|
||||
}
|
||||
|
||||
var seconds = Math.abs(distanceMillis) / 1000;
|
||||
var minutes = seconds / 60;
|
||||
var hours = minutes / 60;
|
||||
var days = hours / 24;
|
||||
var years = days / 365;
|
||||
|
||||
function substitute(stringOrFunction, number) {
|
||||
var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
|
||||
var value = ($l.numbers && $l.numbers[number]) || number;
|
||||
return string.replace(/%d/i, value);
|
||||
}
|
||||
|
||||
var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
|
||||
seconds < 90 && substitute($l.minute, 1) ||
|
||||
minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
|
||||
minutes < 90 && substitute($l.hour, 1) ||
|
||||
hours < 24 && substitute($l.hours, Math.round(hours)) ||
|
||||
hours < 42 && substitute($l.day, 1) ||
|
||||
days < 30 && substitute($l.days, Math.round(days)) ||
|
||||
days < 45 && substitute($l.month, 1) ||
|
||||
days < 365 && substitute($l.months, Math.round(days / 30)) ||
|
||||
years < 1.5 && substitute($l.year, 1) ||
|
||||
substitute($l.years, Math.round(years));
|
||||
|
||||
var separator = $l.wordSeparator || "";
|
||||
if ($l.wordSeparator === undefined) { separator = " "; }
|
||||
return $.trim([prefix, words, suffix].join(separator));
|
||||
},
|
||||
|
||||
parse: function(iso8601) {
|
||||
var s = $.trim(iso8601);
|
||||
s = s.replace(/\.\d+/,""); // remove milliseconds
|
||||
s = s.replace(/-/,"/").replace(/-/,"/");
|
||||
s = s.replace(/T/," ").replace(/Z/," UTC");
|
||||
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
|
||||
s = s.replace(/([\+\-]\d\d)$/," $100"); // +09 -> +0900
|
||||
return new Date(s);
|
||||
},
|
||||
datetime: function(elem) {
|
||||
var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
|
||||
return $t.parse(iso8601);
|
||||
},
|
||||
isTime: function(elem) {
|
||||
// jQuery's `is()` doesn't play well with HTML5 in IE
|
||||
return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
|
||||
}
|
||||
});
|
||||
|
||||
// functions that can be called via $(el).timeago('action')
|
||||
// init is default when no action is given
|
||||
// functions are called with context of a single element
|
||||
var functions = {
|
||||
init: function(){
|
||||
var refresh_el = $.proxy(refresh, this);
|
||||
refresh_el();
|
||||
var $s = $t.settings;
|
||||
if ($s.refreshMillis > 0) {
|
||||
this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis);
|
||||
}
|
||||
},
|
||||
update: function(time){
|
||||
var parsedTime = $t.parse(time);
|
||||
$(this).data('timeago', { datetime: parsedTime });
|
||||
if($t.settings.localeTitle) $(this).attr("title", parsedTime.toLocaleString());
|
||||
refresh.apply(this);
|
||||
},
|
||||
updateFromDOM: function(){
|
||||
$(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) });
|
||||
refresh.apply(this);
|
||||
},
|
||||
dispose: function () {
|
||||
if (this._timeagoInterval) {
|
||||
window.clearInterval(this._timeagoInterval);
|
||||
this._timeagoInterval = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$.fn.timeago = function(action, options) {
|
||||
var fn = action ? functions[action] : functions.init;
|
||||
if(!fn){
|
||||
throw new Error("Unknown function name '"+ action +"' for timeago");
|
||||
}
|
||||
// each over objects here and call the requested function
|
||||
this.each(function(){
|
||||
fn.call(this, options);
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
function refresh() {
|
||||
var data = prepareData(this);
|
||||
var $s = $t.settings;
|
||||
|
||||
if (!isNaN(data.datetime)) {
|
||||
if ( $s.cutoff == 0 || Math.abs(distance(data.datetime)) < $s.cutoff) {
|
||||
$(this).text(inWords(data.datetime));
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
function prepareData(element) {
|
||||
element = $(element);
|
||||
if (!element.data("timeago")) {
|
||||
element.data("timeago", { datetime: $t.datetime(element) });
|
||||
var text = $.trim(element.text());
|
||||
if ($t.settings.localeTitle) {
|
||||
element.attr("title", element.data('timeago').datetime.toLocaleString());
|
||||
} else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
|
||||
element.attr("title", text);
|
||||
}
|
||||
}
|
||||
return element.data("timeago");
|
||||
}
|
||||
|
||||
function inWords(date) {
|
||||
return $t.inWords(distance(date));
|
||||
}
|
||||
|
||||
function distance(date) {
|
||||
return (new Date().getTime() - date.getTime());
|
||||
}
|
||||
|
||||
// fix for IE6 suckage
|
||||
document.createElement("abbr");
|
||||
document.createElement("time");
|
||||
}));
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"public-homepage": [
|
||||
{
|
||||
"title": "Public links go here",
|
||||
"link": "https://github.com/"
|
||||
}
|
||||
],
|
||||
"footer": {
|
||||
"Open Source Portal": [
|
||||
{
|
||||
"title": "Information about the portal",
|
||||
"link": "about:blank"
|
||||
}
|
||||
],
|
||||
"Open Source Links": [
|
||||
{
|
||||
"title": "Open Source",
|
||||
"link": "https://osstool.redmond.corp.microsoft.com:8888/palamida/Home.htm"
|
||||
}
|
||||
],
|
||||
"All Git + GitHub": [
|
||||
{
|
||||
"title": "GitHub Workflow",
|
||||
"link": "https://guides.github.com/introduction/flow/"
|
||||
},
|
||||
{
|
||||
"title": "Git Book",
|
||||
"link": "https://git-scm.com/book/en/v2"
|
||||
},
|
||||
{
|
||||
"title": "A successful Git branching model",
|
||||
"link": "http://nvie.com/posts/a-successful-git-branching-model/"
|
||||
},
|
||||
{
|
||||
"title": "Is your project welcoming?",
|
||||
"link": "http://www.erikaheidi.com/blog/is-your-open-source-project-welcoming-to-new-contributors"
|
||||
}
|
||||
]
|
||||
},
|
||||
"onboarding-complete": {
|
||||
"all": [
|
||||
{
|
||||
"title": "A link to show everyone after onboarding",
|
||||
"link": "https://nytimes.com",
|
||||
"text": "Information about why the link is important."
|
||||
}
|
||||
],
|
||||
"github": [
|
||||
{
|
||||
"title": "GitHub Workflow",
|
||||
"link": "https://guides.github.com/introduction/flow/"
|
||||
},
|
||||
{
|
||||
"title": "Git Book",
|
||||
"link": "https://git-scm.com/book/en/v2"
|
||||
},
|
||||
{
|
||||
"title": "A successful Git branching model",
|
||||
"link": "http://nvie.com/posts/a-successful-git-branching-model/"
|
||||
}
|
||||
],
|
||||
"specific-org-name": [
|
||||
{
|
||||
"title": "An org-specific link for the org named specific-org-name",
|
||||
"link": "about:blank",
|
||||
"text": "About this org-specific link."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var github = require('octonode');
|
||||
var utils = require('../utils');
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var dc = req.app.settings.dataclient;
|
||||
var oss = req.oss;
|
||||
oss.addBreadcrumb(req, 'Requests');
|
||||
async.parallel({
|
||||
ownedTeams: function (callback) {
|
||||
oss.getMyTeamMemberships('maintainer', function (err, ownedTeams) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (ownedTeams && ownedTeams.length && ownedTeams.length > 0) {
|
||||
dc.getPendingApprovals(ownedTeams, function (error, appvs) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
async.each(appvs, function (approval, cb) {
|
||||
var teamFromRequest = approval.teamid;
|
||||
if (teamFromRequest) {
|
||||
oss.getTeam(teamFromRequest, function (err, teamInstance) {
|
||||
approval._teamInstance = teamInstance;
|
||||
cb(err);
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}, function (err) {
|
||||
callback(null, appvs);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
requestsUserMade: function (callback) {
|
||||
// CONSIDER: Need to hydrate with _teamInstance just like above...
|
||||
dc.getPendingApprovalsForUserId(req.user.github.id, callback);
|
||||
}
|
||||
}, function (error, results) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
async.each(results.requestsUserMade, function (request, cb) {
|
||||
var teamFromRequest = request.teamid;
|
||||
if (teamFromRequest) {
|
||||
oss.getTeam(teamFromRequest, function (err, teamInstance) {
|
||||
request._teamInstance = teamInstance;
|
||||
cb(err);
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
oss.render(req, res, 'org/approvals', 'Review My Approvals', {
|
||||
teamResponsibilities: results.ownedTeams,
|
||||
usersRequests: results.requestsUserMade
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:requestid/cancel', function (req, res, next) {
|
||||
var oss = req.oss;
|
||||
var dc = req.app.settings.dataclient;
|
||||
var requestid = req.params.requestid;
|
||||
dc.getApprovalRequest(requestid, function (error, pendingRequest) {
|
||||
if (error) {
|
||||
return next(new Error('The pending request you are looking for does not seem to exist.'));
|
||||
}
|
||||
if (pendingRequest.ghid == req.user.github.id) {
|
||||
dc.updateApprovalRequest(requestid, {
|
||||
active: false,
|
||||
decision: 'canceled-by-user',
|
||||
decisionTime: (new Date().getTime()).toString()
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
oss.getTeam(pendingRequest.teamid, function (error, team) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'We could not get an instance of the team.'));
|
||||
}
|
||||
var workflowRepo = team.org.getWorkflowRepository();
|
||||
var trackingIssue = workflowRepo.issue(pendingRequest.issue);
|
||||
trackingIssue.createComment('This request was canceled by ' + req.user.github.username + ' via the open source portal and can be ignored.', function (ignoredError) {
|
||||
// We ignore any error from the comment field, since that isn't the important part...
|
||||
trackingIssue.close(function (error) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'We had trouble closing the issue. Please take a look or report this as a bug.'));
|
||||
}
|
||||
res.redirect('/approvals/');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return next(new Error('You are not authorized to cancel this request.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:requestid', function (req, res, next) {
|
||||
var oss = req.oss;
|
||||
var requestid = req.params.requestid;
|
||||
var dc = oss.dataClient();
|
||||
oss.addBreadcrumb(req, 'Your Request');
|
||||
var isMaintainer = false, pendingRequest = null, team = null, maintainers = null;
|
||||
async.waterfall([
|
||||
function (callback) {
|
||||
dc.getApprovalRequest(requestid, callback);
|
||||
},
|
||||
function (pendingRequestValue) {
|
||||
var callback = arguments[arguments.length - 1];
|
||||
pendingRequest = pendingRequestValue;
|
||||
oss.getTeam(pendingRequest.teamid, callback);
|
||||
},
|
||||
function (teamValue, callback) {
|
||||
team = teamValue;
|
||||
team.org.isUserSudoer(callback);
|
||||
},
|
||||
function (isOrgSudoer, callback) {
|
||||
isMaintainer = isOrgSudoer;
|
||||
team.getOfficialMaintainers(callback);
|
||||
},
|
||||
function (maintainersValue, callback) {
|
||||
maintainers = maintainersValue;
|
||||
if (!isMaintainer) {
|
||||
for (var i = 0; i < maintainers.length; i++) {
|
||||
if (maintainers[i].id == oss.id.github) {
|
||||
isMaintainer = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isMaintainer) {
|
||||
var err = new Error('Redirecting to the admin experience to approve');
|
||||
var slugPreferred = team.slug || team.name;
|
||||
err.redirect = '/' + team.org.name + '/teams/' + slugPreferred + '/approvals/' + requestid;
|
||||
return callback(err);
|
||||
}
|
||||
if (pendingRequest.ghid != oss.id.github) {
|
||||
var msg = new Error('This request does not exist or was created by another user.');
|
||||
msg.skipLog = true;
|
||||
return callback(msg);
|
||||
}
|
||||
callback();
|
||||
}
|
||||
], function (error) {
|
||||
if (error) {
|
||||
if (error.redirect) {
|
||||
return res.redirect(error.redirect);
|
||||
}
|
||||
// Edge case: the team no longer exists.
|
||||
if (error.innerError && error.innerError.innerError && error.innerError.innerError.statusCode == 404) {
|
||||
var dc = req.app.settings.dataclient;
|
||||
return closeOldRequest(dc, oss, pendingRequest, req, res, next);
|
||||
}
|
||||
return next(error);
|
||||
} else {
|
||||
if (pendingRequest.decisionTime) {
|
||||
var asInt = parseInt(pendingRequest.decisionTime, 10);
|
||||
pendingRequest.decisionTime = new Date(asInt);
|
||||
}
|
||||
oss.render(req, res, 'org/userApprovalStatus', 'Review your request', {
|
||||
entry: pendingRequest,
|
||||
team: team,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function closeOldRequest(dc, oss, pendingRequest, req, res, next) {
|
||||
var org = oss.org(pendingRequest.org);
|
||||
var notificationRepo = org.getWorkflowRepository();
|
||||
if (pendingRequest.active === false) {
|
||||
oss.saveUserAlert(req, 'The team this request was for no longer exists.', 'Team gone!', 'success');
|
||||
return res.redirect('/');
|
||||
}
|
||||
var issue = notificationRepo.issue(pendingRequest.issue);
|
||||
issue.createComment('The team no longer exists on GitHub. This issue is being canceled.', function (error) {
|
||||
if (error) {
|
||||
next(error);
|
||||
}
|
||||
dc.updateApprovalRequest(pendingRequest.RowKey, {
|
||||
active: false,
|
||||
decisionNote: 'Team no longer exists.',
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
next(error);
|
||||
}
|
||||
issue.close(function () {
|
||||
oss.saveUserAlert(req, 'The team this request was for no longer exists. The request has been canceled.', 'Team gone!', 'success');
|
||||
res.redirect('/');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,165 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var moment = require('moment');
|
||||
var utils = require('../utils');
|
||||
var OpenSourceUserContext = require('../oss');
|
||||
|
||||
var linkRoute = require('./link');
|
||||
var linkedUserRoute = require('./index-linked');
|
||||
|
||||
router.use(function (req, res, next) {
|
||||
if (req.isAuthenticated()) {
|
||||
if (req.user && req.user.github && !req.user.github.id) {
|
||||
return next(new Error('Invalid GitHub user information provided by GitHub.'));
|
||||
}
|
||||
var config = req.app.settings.runtimeConfig;
|
||||
var dc = req.app.settings.dataclient;
|
||||
var instance = new OpenSourceUserContext(config, dc, req.user, dc.cleanupInTheFuture.redisClient, function (error) {
|
||||
req.oss = instance;
|
||||
instance.addBreadcrumb(req, 'Organizations');
|
||||
return next();
|
||||
});
|
||||
} else {
|
||||
var url = req.originalUrl;
|
||||
if (url) {
|
||||
if (req.session) {
|
||||
req.session.referer = req.originalUrl;
|
||||
}
|
||||
}
|
||||
res.redirect('/auth/github');
|
||||
}
|
||||
});
|
||||
|
||||
router.use('/link', linkRoute);
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var oss = req.oss;
|
||||
var link = req.oss.entities.link;
|
||||
var dc = req.app.settings.dataclient;
|
||||
var config = req.app.settings.runtimeConfig;
|
||||
var onboarding = req.query.onboarding !== undefined;
|
||||
var allowCaching = onboarding ? false : true;
|
||||
if (!link && req.user.azure === undefined) {
|
||||
return oss.render(req, res, 'welcome', 'Welcome');
|
||||
}
|
||||
if (!link && req.user.azure && req.user.azure.oid) {
|
||||
return res.redirect('/link');
|
||||
}
|
||||
// They're changing their corporate identity (rare, often just service accounts)
|
||||
if (link && link.aadupn && req.user.azure && req.user.azure.username && req.user.azure.username.toLowerCase() !== link.aadupn.toLowerCase()) {
|
||||
return res.redirect('/link/update');
|
||||
}
|
||||
var twoFactorOff = null;
|
||||
var activeOrg = null;
|
||||
async.parallel({
|
||||
isLinkedUser: function (callback) {
|
||||
var link = oss.entities.link;
|
||||
callback(null, link && link.ghu ? link : false);
|
||||
},
|
||||
organizations: function (callback) {
|
||||
oss.getMyOrganizations(allowCaching, function (error, orgsUnsorted) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
async.sortBy(orgsUnsorted, function (org, cb) {
|
||||
cb(null, org.name);
|
||||
}, function (error, orgs) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
// Needs to piggy-back off of any 'active' user...
|
||||
for (var i = 0; i < orgs.length; i++) {
|
||||
if (orgs[i].membershipStateTemporary == 'active') {
|
||||
activeOrg = orgs[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (activeOrg) {
|
||||
activeOrg.queryUserMultifactorStateOkCached(function (error, ok) {
|
||||
twoFactorOff = ok !== true;
|
||||
callback(null, orgs);
|
||||
});
|
||||
} else {
|
||||
callback(null, orgs);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
teamsMaintained: function (callback) {
|
||||
oss.getMyTeamMemberships('maintainer', callback);
|
||||
},
|
||||
userTeamMemberships: function (callback) {
|
||||
oss.getMyTeamMemberships('all', callback);
|
||||
},
|
||||
isAdministrator: function (callback) {
|
||||
callback(null, false);
|
||||
// CONSIDER: Re-implement isAdministrator
|
||||
// oss.isAdministrator(callback);
|
||||
}
|
||||
},
|
||||
function (error, results) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
var i;
|
||||
var countOfOrgs = results.organizations.length;
|
||||
var countOfMemberships = 0;
|
||||
if (results.organizations && results.organizations.length) {
|
||||
for (i = 0; i < results.organizations.length; i++) {
|
||||
if (results.organizations[i].membershipStateTemporary == 'active') {
|
||||
++countOfMemberships;
|
||||
}
|
||||
}
|
||||
}
|
||||
results.countOfOrgs = countOfOrgs;
|
||||
results.countOfMemberships = countOfMemberships;
|
||||
if (countOfMemberships > 0 && twoFactorOff === false) {
|
||||
results.twoFactorOn = true;
|
||||
}
|
||||
if (results.isAdministrator && results.isAdministrator === true) {
|
||||
results.isSudoer = true;
|
||||
}
|
||||
if (results.twoFactorOff === true) {
|
||||
var tempOrgNeedToFix = oss.org();
|
||||
console.log('2fa off, security check time');
|
||||
return res.redirect(tempOrgNeedToFix.baseUrl + 'security-check');
|
||||
}
|
||||
if (countOfMemberships === 0 && !onboarding) {
|
||||
onboarding = true;
|
||||
}
|
||||
var render = function (results) {
|
||||
var pageTitle = results && results.userOrgMembership === false ? 'My GitHub Account' : config.companyName + ' - Open Source Portal for GitHub';
|
||||
oss.render(req, res, 'index', pageTitle, {
|
||||
accountInfo: results,
|
||||
onboarding: onboarding,
|
||||
onboardingPostfixUrl: onboarding === true ? '?onboarding=' + config.companyName : '',
|
||||
activeOrgUrl: activeOrg ? activeOrg.baseUrl : '/?',
|
||||
});
|
||||
};
|
||||
var teamsMaintained = results.teamsMaintained;
|
||||
if (teamsMaintained && teamsMaintained.length && teamsMaintained.length > 0) {
|
||||
var teamsMaintainedHash = {};
|
||||
for (i = 0; i < teamsMaintained.length; i++) {
|
||||
teamsMaintainedHash[teamsMaintained[i].id] = teamsMaintained[i];
|
||||
}
|
||||
results.teamsMaintainedHash = teamsMaintainedHash;
|
||||
dc.getPendingApprovals(teamsMaintained, function (error, pendingApprovals) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
results.pendingApprovals = pendingApprovals;
|
||||
render(results);
|
||||
});
|
||||
} else render(results);
|
||||
});
|
||||
});
|
||||
|
||||
router.use(linkedUserRoute);
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,84 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var moment = require('moment');
|
||||
var utils = require('../utils');
|
||||
var OpenSourceUserContext = require('../oss');
|
||||
|
||||
var orgsRoute = require('./orgs');
|
||||
var orgAdmin = require('./orgAdmin');
|
||||
var approvalsSystem = require('./approvals');
|
||||
var linkRoute = require('./link');
|
||||
var unlinkRoute = require('./unlink');
|
||||
var legacyRoute = require('./legacy');
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
// SECURITY ROUTE MARKER:
|
||||
// Below this next call, all routes will require an active link to exist for
|
||||
// the authenticated GitHub user.
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
router.use(function (req, res, next) {
|
||||
var link = req.oss.entities.link;
|
||||
if (link && link.ghu) {
|
||||
next();
|
||||
} else {
|
||||
var error = new Error('Not found (not a corporate authenticated user).');
|
||||
error.status = 404;
|
||||
error.originalUrl = req.originalUrl;
|
||||
error.skipLog = true;
|
||||
error.detailed = 'You are not currently signed in as a user with a "linked" corporate identity, FYI.';
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
// end security route
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
router.use('/unlink', unlinkRoute);
|
||||
|
||||
router.get('/teams', function (req, res, next) {
|
||||
var oss = req.oss;
|
||||
var i;
|
||||
oss.addBreadcrumb(req, 'All Teams');
|
||||
async.parallel({
|
||||
allTeams: oss.getAllOrganizationsTeams.bind(oss),
|
||||
userTeams: oss.getMyTeamMemberships.bind(oss, 'all'),
|
||||
}, function (error, r) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
var highlightedTeams = [];
|
||||
var orgs = oss.orgs();
|
||||
for (i = 0; i < orgs.length; i++) {
|
||||
var highlighted = orgs[i].getHighlightedTeams();
|
||||
for (var j = 0; j < highlighted.length; j++) {
|
||||
highlightedTeams.push(highlighted[j]);
|
||||
}
|
||||
}
|
||||
var userTeamsById = {};
|
||||
for (i = 0; i < r.userTeams.length; i++) {
|
||||
userTeamsById[r.userTeams[i].id] = true;
|
||||
}
|
||||
for (i = 0; i < r.allTeams.length; i++) {
|
||||
r.allTeams[i]._hack_isMember = userTeamsById[r.allTeams[i].id] ? true : false;
|
||||
}
|
||||
oss.render(req, res, 'org/teams', 'Teams', {
|
||||
availableTeams: r.allTeams,
|
||||
highlightedTeams: highlightedTeams,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.use('/organization', orgAdmin);
|
||||
router.use('/approvals', approvalsSystem);
|
||||
router.use(legacyRoute);
|
||||
router.use('/', orgsRoute);
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
|
||||
router.get('/', function(req, res, next) {
|
||||
if (!req.isAuthenticated()) {
|
||||
var config = req.app.settings.runtimeConfig;
|
||||
return res.render('home', {
|
||||
user: req.user,
|
||||
config: config,
|
||||
corporateLinks: config.corporate.trainingResources['public-homepage'],
|
||||
serviceBanner: config && config.serviceBanner ? config.serviceBanner : undefined,
|
||||
title: 'Open Source Portal for GitHub - ' + config.companyName});
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
router.use('/thanks', require('./thanks'));
|
||||
router.use(require('./microsoft-specific'));
|
||||
|
||||
router.use(require('./index-authenticated'));
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var utils = require('../utils');
|
||||
|
||||
// This file helps manage URI changes that happened in October 2015.
|
||||
// Not necessary for new instances of the portal.
|
||||
|
||||
router.use('/account/jointeams', function (req, res, next) {
|
||||
res.redirect('/teams');
|
||||
});
|
||||
|
||||
router.use('/team/:teamid', function (req, res, next) {
|
||||
var oss = req.oss;
|
||||
var teamid = req.params.teamid;
|
||||
oss.getTeam(teamid, function (error, team) {
|
||||
if (error) {
|
||||
var err = utils.wrapError(error, 'Team not found.', true);
|
||||
err.status = 404;
|
||||
return next(err);
|
||||
}
|
||||
req.team = team;
|
||||
next();
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/team/:teamid/approvals/:approvalid', function (req, res, next) {
|
||||
var team = req.team;
|
||||
res.redirect(team.org.baseUrl + 'teams/' + team.slug + '/approvals/' + req.params.approvalid);
|
||||
});
|
||||
|
||||
router.get('/account/approvals/:approvalid', function (req, res, next) {
|
||||
res.redirect('/approvals/' + req.params.approvalid);
|
||||
});
|
||||
|
||||
router.get('/team/:teamid', function (req, res, next) {
|
||||
var team = req.team;
|
||||
res.redirect(team.org.baseUrl + 'teams/' + team.slug);
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var utils = require('../utils');
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var oss = req.oss;
|
||||
if (!(oss.usernames.azure && oss.usernames.github)) {
|
||||
return next(new Error('You must be signed in to both Active Directory and your GitHub account in order to link your account.'));
|
||||
}
|
||||
if (!req.oss.entities.link) {
|
||||
req.oss.render(req, res, 'link', 'Link GitHub with corporate identity ' + req.oss.usernames.azure);
|
||||
} else {
|
||||
return res.redirect('/');
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', function (req, res, next) {
|
||||
var dc = req.app.settings.dataclient;
|
||||
dc.createLinkObjectFromRequest(req, function (error, linkObject, callback) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'We had trouble linking your corporate and GitHub accounts.'));
|
||||
}
|
||||
dc.insertLink(req.user.github.id, linkObject, function (error, result, response) {
|
||||
if (error) {
|
||||
// There are legacy upgrade scenarios for some users where they already have a
|
||||
// link, even though they are already on this page. In that case, we just do
|
||||
// a retroactive upsert.
|
||||
dc.updateLink(req.user.github.id, linkObject, function (error2) {
|
||||
if (error2) {
|
||||
error2.original = error;
|
||||
return next(utils.wrapError(error2, 'We had trouble storing the corporate identity link information. Please file this issue and we will have an administrator take a look.'));
|
||||
}
|
||||
return res.redirect('/?onboarding=yes');
|
||||
});
|
||||
} else {
|
||||
return res.redirect('/?onboarding=yes');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/update', function (req, res, next) {
|
||||
var oss = req.oss;
|
||||
if (!(oss.usernames.azure)) {
|
||||
return oss.render(req, res, 'linkUpdate', 'Update your account ' + oss.usernames.github + ' by signing in with corporate credentials.');
|
||||
}
|
||||
var dc = req.app.settings.dataclient;
|
||||
dc.createLinkObjectFromRequest(req, function (error, linkObject, callback) {
|
||||
dc.updateLink(req.user.github.id, linkObject, function (error) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'We had trouble updating the link using a data store API.'));
|
||||
}
|
||||
oss.saveUserAlert(req, 'Your GitHub account is now associated with the corporate identity for ' + linkObject.aadupn + '.', 'Corporate Identity Link Updated', 'success');
|
||||
res.redirect('/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var moment = require('moment');
|
||||
var utils = require('../../utils');
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var org = req.org;
|
||||
var onboarding = req.query.onboarding;
|
||||
var joining = req.query.joining;
|
||||
org.oss.addBreadcrumb(req, 'Multi-factor authentication check');
|
||||
org.queryUserMultifactorStateOk(function (error, state) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'A problem occurred while trying to query important compliance information regarding your account.'));
|
||||
}
|
||||
if (state === true && (req.body.validate || onboarding || joining)) {
|
||||
var url = org.baseUrl;
|
||||
if (onboarding || joining) {
|
||||
var urlSegment = '?' + (onboarding ? 'onboarding' : 'joining') + '=' + (onboarding ? onboarding : joining);
|
||||
url = org.baseUrl +
|
||||
(onboarding ? 'profile-review' : 'teams') +
|
||||
urlSegment;
|
||||
}
|
||||
return res.redirect(url);
|
||||
}
|
||||
var title = state === false ? 'Please enable two-factor authentication now' : 'Thanks for using modern security practices';
|
||||
req.oss.render(req, res, 'org/2fa', title, {
|
||||
twoFactorOff: ! state,
|
||||
notValidated: (req.query.validate ? true : undefined),
|
||||
onboarding: onboarding,
|
||||
org: org,
|
||||
nowString: moment().format('MMMM Do YYYY, h:mm:ss a'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,110 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var utils = require('../../utils');
|
||||
|
||||
var teamsRoute = require('./teams');
|
||||
var membershipRoute = require('./membership');
|
||||
var joinRoute = require('./join');
|
||||
var leaveRoute = require('./leave');
|
||||
var securityCheckRoute = require('./2fa');
|
||||
var profileReviewRoute = require('./profileReview');
|
||||
var approvalsSystem = require('../approvals');
|
||||
var requestRepo = require('./requestRepo');
|
||||
|
||||
router.use(function (req, res, next) {
|
||||
var onboarding = req.query.onboarding;
|
||||
req.org.oss.addBreadcrumb(req, req.org.name, onboarding ? false : undefined);
|
||||
next();
|
||||
});
|
||||
|
||||
router.use('/join', joinRoute);
|
||||
|
||||
// Org membership requirement middleware
|
||||
|
||||
router.use(function (req, res, next) {
|
||||
var org = req.org;
|
||||
org.queryUserMembership(function (error, result) {
|
||||
if (result && result.state && result.state == 'active') {
|
||||
next();
|
||||
} else {
|
||||
res.redirect(org.baseUrl + 'join');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Org membership required endppoints:
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var org = req.org;
|
||||
var oss = req.oss;
|
||||
var dc = req.app.settings.dataclient;
|
||||
async.parallel({
|
||||
teamsMaintained: function (callback) {
|
||||
org.getMyTeamMemberships('maintainer', callback);
|
||||
},
|
||||
userTeamMemberships: function (callback) {
|
||||
org.getMyTeamMemberships('all', callback);
|
||||
},
|
||||
isMembershipPublic: function (callback) {
|
||||
org.queryUserPublicMembership(callback);
|
||||
},
|
||||
orgUser: function (callback) {
|
||||
org.getDetails(function (error, details) {
|
||||
var userDetails = details ? org.oss.user(details.id, details) : null;
|
||||
callback(error, userDetails);
|
||||
});
|
||||
},
|
||||
/*
|
||||
CONSIDER: UPDATE ORG SUDOERS SYSTEM UI...
|
||||
isAdministrator: function (callback) {
|
||||
oss.isAdministrator(callback);
|
||||
}*/
|
||||
},
|
||||
function (error, results) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
if (results.isAdministrator && results.isAdministrator === true) {
|
||||
results.isSudoer = true;
|
||||
}
|
||||
var render = function (results) {
|
||||
oss.render(req, res, 'org/index', org.name, {
|
||||
accountInfo: results,
|
||||
org: org,
|
||||
});
|
||||
};
|
||||
// Check for pending approvals
|
||||
var teamsMaintained = results.teamsMaintained;
|
||||
if (teamsMaintained && teamsMaintained.length && teamsMaintained.length > 0) {
|
||||
var teamsMaintainedHash = {};
|
||||
for (var i = 0; i < teamsMaintained.length; i++) {
|
||||
teamsMaintainedHash[teamsMaintained[i].id] = teamsMaintained[i];
|
||||
}
|
||||
results.teamsMaintainedHash = teamsMaintainedHash;
|
||||
dc.getPendingApprovals(teamsMaintained, function (error, pendingApprovals) {
|
||||
if (!error && pendingApprovals) {
|
||||
results.pendingApprovals = pendingApprovals;
|
||||
}
|
||||
render(results);
|
||||
});
|
||||
} else {
|
||||
render(results);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.use('/membership', membershipRoute);
|
||||
router.use('/leave', leaveRoute);
|
||||
router.use('/teams', teamsRoute);
|
||||
router.use('/security-check', securityCheckRoute);
|
||||
router.use('/profile-review', profileReviewRoute);
|
||||
router.use('/approvals', approvalsSystem);
|
||||
router.use('/new-repo', requestRepo);
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,92 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var utils = require('../../utils');
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var org = req.org;
|
||||
var onboarding = req.query.onboarding;
|
||||
org.queryUserMembership(false /* do not allow caching */, function (error, result) {
|
||||
var state = result && result.state ? result.state : false;
|
||||
var clearAuditListAndRedirect = function () {
|
||||
org.clearAuditList(function () {
|
||||
var url = org.baseUrl + 'security-check' + (onboarding ? '?onboarding=' + onboarding : '?joining=' + org.name);
|
||||
res.redirect(url);
|
||||
});
|
||||
};
|
||||
var showPage = function () {
|
||||
org.getDetails(function (error, details) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
var userDetails = details ? org.oss.user(details.id, details) : null;
|
||||
var title = org.name + ' Organization Membership ' + (state == 'pending' ? 'Pending' : 'Join');
|
||||
req.oss.render(req, res, 'org/pending', title, {
|
||||
result: result,
|
||||
state: state,
|
||||
org: org,
|
||||
orgUser: userDetails,
|
||||
onboarding: onboarding,
|
||||
});
|
||||
});
|
||||
};
|
||||
if (state == 'active') {
|
||||
clearAuditListAndRedirect();
|
||||
} else if (state == 'pending' && req.user.github.increasedScope) {
|
||||
var userToken = req.user.github.increasedScope.github.accessToken;
|
||||
org.acceptOrganizationInvitation(userToken, function (error, updatedState) {
|
||||
if (error) {
|
||||
if (error.statusCode == 401) {
|
||||
req.session.referer = req.originalUrl;
|
||||
return res.redirect('/auth/github/increased-scope');
|
||||
}
|
||||
// We do not error out, they can still fall back on the
|
||||
// manual acceptance system that the page will render.
|
||||
// CONSIDER: Log this error anyway for investigation...
|
||||
}
|
||||
if (!error && updatedState && updatedState.state === 'active') {
|
||||
return clearAuditListAndRedirect();
|
||||
}
|
||||
showPage();
|
||||
});
|
||||
} else {
|
||||
showPage();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/express', function (req, res, next) {
|
||||
var org = req.org;
|
||||
var onboarding = req.query.onboarding;
|
||||
org.queryUserMembership(false /* do not allow caching */, function (error, result) {
|
||||
var state = result && result.state ? result.state : false;
|
||||
if (state == 'active'|| state == 'pending') {
|
||||
res.redirect(org.baseUrl + 'join' + (onboarding ? '?onboarding=' + onboarding : '?joining=' + org.name));
|
||||
} else if (req.user.github.increasedScope && req.user.github.increasedScope.github && req.user.github.increasedScope.github.accessToken) {
|
||||
joinOrg(req, res, next);
|
||||
} else {
|
||||
req.session.referer = req.originalUrl;
|
||||
res.redirect('/auth/github/increased-scope');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function joinOrg(req, res, next) {
|
||||
var org = req.org;
|
||||
var onboarding = req.query.onboarding;
|
||||
var everyoneTeam = org.getAllMembersTeam();
|
||||
everyoneTeam.addMembership('member', function (error) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'We had trouble sending you an invitation through GitHub to join the ' + org.name + ' organization. Please try again later. If you continue to receive this message, please reach out for us to investigate.'));
|
||||
}
|
||||
res.redirect(org.baseUrl + 'join' + (onboarding ? '?onboarding=' + onboarding : '?joining=' + org.name));
|
||||
});
|
||||
}
|
||||
|
||||
router.post('/', joinOrg);
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var moment = require('moment');
|
||||
var utils = require('../../utils');
|
||||
|
||||
router.use(function (req, res, next) {
|
||||
var org = req.org;
|
||||
req.orgLeave = {
|
||||
state: null,
|
||||
};
|
||||
var memberOfOrgs = [];
|
||||
async.each(org.oss.orgs(), function (o, callback) {
|
||||
o.queryUserMembership(false /* no caching */, function (error, result) {
|
||||
var state = null;
|
||||
if (result && result.state) {
|
||||
state = result.state;
|
||||
}
|
||||
// This specific org...
|
||||
if (o.name == org.name) {
|
||||
req.orgLeave.state = state;
|
||||
}
|
||||
if (state == 'active' || state == 'pending') {
|
||||
memberOfOrgs.push({
|
||||
state: state,
|
||||
org: o,
|
||||
});
|
||||
}
|
||||
callback(error);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
if (!req.orgLeave.state) {
|
||||
return res.redirect('/');
|
||||
} else {
|
||||
req.orgLeave.memberOfOrgs = memberOfOrgs;
|
||||
org.oss.addBreadcrumb(req, 'Leave');
|
||||
next();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var org = req.org;
|
||||
var orgs = req.orgLeave.memberOfOrgs;
|
||||
// Unlink is more severe and will handle removing the remaining organization membership...
|
||||
if (orgs.length < 2) {
|
||||
return res.redirect('/unlink');
|
||||
}
|
||||
req.oss.render(req, res, 'org/leave', 'Leave ' + org.name, {
|
||||
org: org,
|
||||
orgs: orgs,
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/', function (req, res, next) {
|
||||
var org = req.org;
|
||||
var orgs = req.orgLeave.memberOfOrgs;
|
||||
if (orgs.length < 2) {
|
||||
return res.redirect('/unlink');
|
||||
}
|
||||
org.removeUserMembership(function (error) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'We received an error code back from GitHub when trying to remove your membership from ' + org.name + '.'));
|
||||
}
|
||||
req.oss.saveUserAlert(req, 'Your ' + org.name + ' membership has been canceled at your request.', org.name, 'success');
|
||||
res.redirect('/');
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var utils = require('../../utils');
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var org = req.org ? req.org : req.oss.org();
|
||||
var onboarding = req.query.onboarding;
|
||||
var joining = req.query.joining;
|
||||
org.queryUserPublicMembership(function (error, result) {
|
||||
var publicMembership = result === true;
|
||||
org.oss.addBreadcrumb(req, 'Membership Visibility');
|
||||
var teamPostfix = '';
|
||||
if (onboarding || joining) {
|
||||
teamPostfix = '?' + (onboarding ? 'onboarding' : 'joining') + '=' + (onboarding || joining);
|
||||
}
|
||||
req.oss.render(req, res, 'org/publicMembershipStatus', org.name + ' Membership Visibility', {
|
||||
org: org,
|
||||
publicMembership: publicMembership,
|
||||
theirUsername: req.oss.usernames.github,
|
||||
onboarding: onboarding,
|
||||
joining: joining,
|
||||
teamPostfix: teamPostfix,
|
||||
showBreadcrumbs: onboarding === undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/', function (req, res, next) {
|
||||
var user = req.user;
|
||||
var onboarding = req.query.onboarding;
|
||||
var joining = req.query.joining;
|
||||
if (user && user.github && user.github.increasedScope && user.github.increasedScope.github && user.github.increasedScope.github.accessToken) {
|
||||
var org = req.org ? req.org : req.oss.org();
|
||||
var message1 = req.body.conceal ? 'concealing' : 'publicizing';
|
||||
var message2 = req.body.conceal ? 'hidden' : 'public, thanks for your support';
|
||||
org[req.body.conceal ? 'setPrivateMembership' : 'setPublicMembership'].call(org, user.github.increasedScope.github.accessToken, function (error) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'We had trouble ' + message1 + ' your membership. Did you authorize the increased scope of access with GitHub?'));
|
||||
}
|
||||
req.oss.saveUserAlert(req, 'Your ' + org.name + ' membership is now ' + message2 + '!', org.name, 'success');
|
||||
var url = org.baseUrl + ((onboarding || joining) ? '/teams' : '');
|
||||
var extraUrl = (onboarding || joining) ? '?' + (onboarding ? 'onboarding' : 'joining') + '=' + (onboarding || joining) : '';
|
||||
res.redirect(url + extraUrl);
|
||||
});
|
||||
} else {
|
||||
return next(new Error('The increased scope to write the membership to GitHub was not found in your session. Please report this error.'));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var utils = require('../../utils');
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var org = req.org;
|
||||
var onboarding = req.query.onboarding;
|
||||
org.oss.modernUser().getDetailsByUsername(function () {
|
||||
var detailed = org.oss.modernUser();
|
||||
var userProfileWarnings = {};
|
||||
if (!detailed.company || (detailed.company && detailed.company.toLowerCase().indexOf(org.oss.setting('companyName').toLowerCase()) < 0)) {
|
||||
userProfileWarnings.company = 'color:red';
|
||||
}
|
||||
if (!detailed.email || (detailed.email && detailed.email.toLowerCase().indexOf(org.oss.setting('companyName').toLowerCase()) < 0)) {
|
||||
userProfileWarnings.email = 'color:red';
|
||||
}
|
||||
req.oss.render(req, res, 'org/profileReview', 'Your GitHub Profile', {
|
||||
org: org,
|
||||
userProfile: detailed,
|
||||
userProfileWarnings: userProfileWarnings,
|
||||
theirUsername: req.oss.usernames.github,
|
||||
onboarding: onboarding,
|
||||
showBreadcrumbs: onboarding === undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,190 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var utils = require('../../utils');
|
||||
|
||||
router.use(function (req, res, next) {
|
||||
req.oss.addBreadcrumb(req, 'Request a new repo');
|
||||
next();
|
||||
});
|
||||
|
||||
router.post('/', function (req, res, next) {
|
||||
var org = req.org;
|
||||
var oss = org.oss;
|
||||
if (!req.body.name || (req.body.name.length !== undefined && req.body.name.length === 0)) {
|
||||
return next(new Error('Please provide a repo name.'));
|
||||
}
|
||||
if (req.body.name.indexOf(' ') >= 0) {
|
||||
return next(utils.wrapError(null, 'Repos cannot have spaces in their name. Consider a dash.', true));
|
||||
}
|
||||
if (!req.body.justification || (req.body.justification.length !== undefined && req.body.justification.length === 0)) {
|
||||
return next(utils.wrapError(null, 'A justification is required.', true));
|
||||
}
|
||||
if (!(req.body.visibility == 'public' || req.body.visibility == 'private')) {
|
||||
req.body.visibility = 'public';
|
||||
}
|
||||
if (!req.body.teamCount) {
|
||||
return next(new Error('Invalid.'));
|
||||
}
|
||||
var teamsRequested = [];
|
||||
var teamCount = Math.floor(req.body.teamCount);
|
||||
var i = 0;
|
||||
for (i = 0; i < teamCount; i++) {
|
||||
var existingTeamId = req.body['existingTeam' + i];
|
||||
if (existingTeamId) {
|
||||
existingTeamId = Math.floor(existingTeamId);
|
||||
var perm = req.body['existingTeamPermission' + i];
|
||||
if (existingTeamId > 0 && perm == 'pull' || perm == 'push' || perm == 'admin') {
|
||||
var tr = {
|
||||
id: existingTeamId,
|
||||
permission: perm,
|
||||
};
|
||||
teamsRequested.push(tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
var dc = req.app.settings.dataclient;
|
||||
var team = org.getRepoApproversTeam();
|
||||
var approvalRequest = {
|
||||
ghu: oss.usernames.github,
|
||||
ghid: oss.id.github,
|
||||
justification: req.body.justification,
|
||||
requested: ((new Date()).getTime()).toString(),
|
||||
active: false,
|
||||
teamid: team.id,
|
||||
type: 'repo',
|
||||
org: org.name.toLowerCase(),
|
||||
repoName: req.body.name,
|
||||
repoDescription: req.body.description,
|
||||
repoUrl: req.body.url,
|
||||
repoVisibility: req.body.visibility,
|
||||
email: oss.modernUser().contactEmail(),
|
||||
};
|
||||
approvalRequest.teamsCount = teamsRequested.length;
|
||||
for (i = 0; i < teamsRequested.length; i++) {
|
||||
approvalRequest['teamid' + i] = teamsRequested[i].id;
|
||||
approvalRequest['teamid' + i + 'p'] = teamsRequested[i].permission;
|
||||
}
|
||||
team.getMemberLinks(function (error, maintainers) {
|
||||
if (error) {
|
||||
return next(new Error('It seems that the repo approvers information is unknown, or something happened when trying to query information about the team you are trying to apply to. Please file a bug or try again later. Sorry!'));
|
||||
}
|
||||
if (maintainers === undefined || maintainers.length === undefined || maintainers.length === 0) {
|
||||
return next(new Error('It seems that the repo approvers for this team is unknown. Please file a bug. Thanks.'));
|
||||
}
|
||||
var randomMaintainer = maintainers[Math.floor(Math.random() * maintainers.length)];
|
||||
if (!randomMaintainer.link.ghu) {
|
||||
return next(new Error('For some reason the randomly picked maintainer is not setup in the compliance system properly. Please report this bug.'));
|
||||
}
|
||||
var assignTo = randomMaintainer.link.ghu;
|
||||
var allMaintainers = [];
|
||||
for (var i = 0; i < maintainers.length; i++) {
|
||||
if (maintainers[i].link.ghu) {
|
||||
allMaintainers.push('@' + maintainers[i].link.ghu);
|
||||
}
|
||||
}
|
||||
var consolidatedMaintainers = allMaintainers.join(', ');
|
||||
dc.insertGeneralApprovalRequest('repo', approvalRequest, function (error, requestId) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
var body = 'Hi,\n' + oss.usernames.github + ' has requested a new repo for the ' + org.name + ' ' +
|
||||
'organization.' + '\n\n' +
|
||||
consolidatedMaintainers + ': Can a repo approver for this org review the request now at ' + '\n' +
|
||||
'https://azureopensource.azurewebsites.net/approvals/' + requestId + '?\n\n' +
|
||||
'<small>Note: This issue was generated by the open source portal.</small>' + '\n\n' +
|
||||
'<small>If you use this issue to comment with the team maintainers(s), please understand that your comment will be visible by all members of the organization.</small>';
|
||||
var workflowRepository = org.getWorkflowRepository();
|
||||
workflowRepository.createIssue({
|
||||
title: 'Request to create a repo - ' + oss.usernames.github,
|
||||
body: body,
|
||||
}, function (error, issue, headers) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'A tracking issue could not be created to monitor this request. Please contact the admins and provide this URL to them. Thanks.'));
|
||||
}
|
||||
req.oss.saveUserAlert(req, 'Your repo request has been submitted and will be reviewed by one of the repo approvers for the org for naming consistency, business justification, etc. Thanks!', 'Repo Request Submitted', 'success');
|
||||
if (issue.id && issue.number) {
|
||||
dc.updateApprovalRequest(requestId, {
|
||||
issueid: issue.id.toString(),
|
||||
issue: issue.number.toString(),
|
||||
active: true
|
||||
}, function (error) {
|
||||
workflowRepository.updateIssue(issue.number, {
|
||||
assignee: assignTo,
|
||||
}, function (gitError) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
} else {
|
||||
// CONSIDER: Log gitError. Since assignment fails for users
|
||||
// who have not used the portal, it should not actually
|
||||
// block the workflow from assignment.
|
||||
oss.render(req, res, 'message', 'Repo request submitted', {
|
||||
messageTitle: req.body.name.toUpperCase() + ' REPO',
|
||||
message: 'Your request has been submitted for review to the approvers group for the requested organization.'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return res.redirect('/');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var org = req.org;
|
||||
var orgName = org.name.toLowerCase();
|
||||
var highlightedTeams = org.inner.settings.highlightedTeams;
|
||||
var allowPrivateRepos = org.inner.settings.type == 'publicprivate';
|
||||
org.getTeams(false /* do not use cached */, function (error, teams) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'Could not read the entire list of read (pull) teams from GitHub. Please try again later or report this error if you continue seeing it.'));
|
||||
}
|
||||
var team = org.getRepoApproversTeam();
|
||||
team.getMemberLinks(function (error, approvers) {
|
||||
if (error) {
|
||||
return next(new Error('Could not retrieve the repo approvers for ' + orgName));
|
||||
}
|
||||
var selectTeams = [];
|
||||
var i = 1;
|
||||
var featuredTeamsCount = highlightedTeams.length;
|
||||
for (; i < featuredTeamsCount + 1; i++) {
|
||||
var ht = highlightedTeams[i - 1];
|
||||
ht.number = i;
|
||||
ht.name = org.team(ht.id).name;
|
||||
selectTeams.push(ht);
|
||||
}
|
||||
var allMembersTeam = org.getAllMembersTeam();
|
||||
++featuredTeamsCount;
|
||||
selectTeams.push({
|
||||
number: i++,
|
||||
name: allMembersTeam.name,
|
||||
id: allMembersTeam.id,
|
||||
info: 'This team automatically contains all members of the "' + org.name + '" organization who have linked corporate identities. Broad read access suggested.',
|
||||
});
|
||||
for (; i < featuredTeamsCount + 5; i++) {
|
||||
selectTeams.push({
|
||||
number: i
|
||||
});
|
||||
}
|
||||
org.oss.render(req, res, 'org/requestRepo', 'Request a a new repository', {
|
||||
orgName: orgName,
|
||||
orgConfig: org.inner.settings,
|
||||
allowPrivateRepos: allowPrivateRepos,
|
||||
approvers: approvers,
|
||||
teams: teams,
|
||||
org: org,
|
||||
selectTeams: selectTeams,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,201 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var utils = require('../../../../utils');
|
||||
|
||||
function teamsInfoFromRequest(team, approvalRequest, callback) {
|
||||
var oss = team.oss;
|
||||
if (approvalRequest.teamsCount) {
|
||||
var count = parseInt(approvalRequest.teamsCount, 10);
|
||||
var detailedTeams = [];
|
||||
for (var i = 0; i < count; i++) {
|
||||
var key = 'teamid' + i;
|
||||
if (approvalRequest[key] && approvalRequest[key + 'p']) {
|
||||
detailedTeams.push({
|
||||
id: approvalRequest[key],
|
||||
permission: approvalRequest[key + 'p'],
|
||||
});
|
||||
}
|
||||
}
|
||||
async.map(detailedTeams, function (basic, cb) {
|
||||
var permission = basic.permission;
|
||||
oss.getTeam(basic.id, function (error, teamInstance) {
|
||||
if (teamInstance) {
|
||||
teamInstance._temporary_permission = permission;
|
||||
}
|
||||
cb(error, teamInstance);
|
||||
});
|
||||
}, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var approvalRequest = req.approvalEngine.request;
|
||||
var oss = req.oss;
|
||||
var team = req.team;
|
||||
teamsInfoFromRequest(team, approvalRequest, function (error, expandedTeamInfo) {
|
||||
// Ignoring any errors for now.
|
||||
if (approvalRequest.requested) {
|
||||
var asInt = parseInt(approvalRequest.requested, 10);
|
||||
approvalRequest.requestedTime = new Date(asInt);
|
||||
}
|
||||
if (approvalRequest.decisionTime) {
|
||||
approvalRequest.decisionTime = new Date(parseInt(approvalRequest.decisionTime, 10));
|
||||
}
|
||||
oss.render(req, res, 'org/team/approveStatus', 'Request Status', {
|
||||
entry: approvalRequest,
|
||||
requestingUser: req.approvalEngine.user,
|
||||
expandedTeamInfo: expandedTeamInfo,
|
||||
team: team,
|
||||
teamUrl: req.teamUrl,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/edit', function (req, res, next) {
|
||||
var approvalEngine = req.approvalEngine;
|
||||
if (approvalEngine.editGet) {
|
||||
return approvalEngine.editGet(req, res, next);
|
||||
}
|
||||
next(new Error('Editing is not supported for this request type.'));
|
||||
});
|
||||
|
||||
router.post('/edit', function (req, res, next) {
|
||||
var approvalEngine = req.approvalEngine;
|
||||
if (approvalEngine.editPost) {
|
||||
return approvalEngine.editPost(req, res, next);
|
||||
}
|
||||
next(new Error('Editing is not supported for this request type.'));
|
||||
});
|
||||
|
||||
router.get('/setNote/:action', function (req, res, next) {
|
||||
var engine = req.approvalEngine;
|
||||
var action = req.params.action;
|
||||
if (action == 'approveWithComment') {
|
||||
action = 'approve';
|
||||
}
|
||||
engine.team.oss.render(req, res, 'org/team/approveStatusWithNote', 'Record your comment for request ' + engine.id + ' (' + action + ')', {
|
||||
entry: engine.request,
|
||||
action: action,
|
||||
requestingUser: engine.user,
|
||||
team: req.team,
|
||||
teamUrl: req.teamUrl,
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/', function (req, res, next) {
|
||||
var engine = req.approvalEngine;
|
||||
var requestid = engine.id;
|
||||
var team = engine.team;
|
||||
var org = req.org;
|
||||
var dc = req.app.settings.dataclient;
|
||||
if (! req.body.text && req.body.deny) {
|
||||
return res.redirect(req.teamUrl + 'approvals/' + requestid + '/setNote/deny');
|
||||
}
|
||||
if (req.body.reopen) {
|
||||
req.oss.saveUserAlert(req, 'Request re-opened.', engine.typeName, 'success');
|
||||
return dc.updateApprovalRequest(requestid, {
|
||||
active: true
|
||||
}, function (error) {
|
||||
res.redirect(req.teamUrl + 'approvals/' + requestid);
|
||||
});
|
||||
}
|
||||
if (! req.body.text && req.body.approveWithComment) {
|
||||
return res.redirect(req.teamUrl + 'approvals/' + requestid + '/setNote/approveWithComment');
|
||||
}
|
||||
var action = req.body.approveWithComment || req.body.approve ? 'approve' : 'deny';
|
||||
var bodyText = req.body.text;
|
||||
var oss = req.oss;
|
||||
var notificationRepo = org.getWorkflowRepository();
|
||||
var friendlyErrorMessage = 'Whoa? What happened?';
|
||||
var pendingRequest = engine.request;
|
||||
var issue = null;
|
||||
async.waterfall([
|
||||
function (callback) {
|
||||
issue = notificationRepo.issue(pendingRequest.issue);
|
||||
var bodyAddition = engine.messageForAction(action);
|
||||
if (bodyText !== undefined) {
|
||||
bodyAddition += '\n\nA note was included with the decision and can be viewed by team maintainers and the requesting user.';
|
||||
}
|
||||
var comment = bodyAddition + '\n\n<small>This was generated by the Open Source Portal on behalf of ' +
|
||||
oss.usernames.github + '.</small>';
|
||||
if (pendingRequest.ghu) {
|
||||
comment += '\n\n' + 'FYI, @' + pendingRequest.ghu + '\n';
|
||||
}
|
||||
friendlyErrorMessage = 'While trying to comment on issue #' + issue.number + ', an error occurred.';
|
||||
issue.createComment(comment, callback);
|
||||
},
|
||||
function (comment) {
|
||||
var callback = arguments[arguments.length - 1];
|
||||
var requestUpdates = {
|
||||
decision: action,
|
||||
active: false,
|
||||
decisionTime: (new Date().getTime()).toString(),
|
||||
decisionBy: oss.usernames.github,
|
||||
decisionNote: bodyText,
|
||||
decisionEmail: oss.modernUser().contactEmail(),
|
||||
};
|
||||
friendlyErrorMessage = 'The approval request information could not be updated, indicating a data store problem potentially. The decision may not have been recorded.';
|
||||
dc.updateApprovalRequest(requestid, requestUpdates, callback);
|
||||
},
|
||||
function () {
|
||||
var callback = arguments[arguments.length - 1];
|
||||
if (action == 'approve') {
|
||||
engine.performApprovalOperation(callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
function closeIssue() {
|
||||
var callback = arguments[arguments.length - 1];
|
||||
friendlyErrorMessage = 'The issue #' + issue.number + ' that tracks the request could not be closed.';
|
||||
issue.close(callback);
|
||||
},
|
||||
function () {
|
||||
friendlyErrorMessage = null;
|
||||
var callback = arguments[arguments.length - 1];
|
||||
if (action == 'approve' && engine.generateSecondaryTasks) {
|
||||
engine.generateSecondaryTasks(callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
// Secondary tasks run after the primary and in general will not
|
||||
// fail the approval operation. By sending an empty error callback
|
||||
// but then an object with an error property set, the operation
|
||||
// that failed can report status. Whether an error or not, a
|
||||
// message property will be shown for each task result.
|
||||
function () {
|
||||
friendlyErrorMessage = null;
|
||||
var tasks = arguments.length > 1 ? arguments[0] : [];
|
||||
var callback = arguments[arguments.length - 1];
|
||||
async.series(tasks, callback);
|
||||
},
|
||||
], function (error, output) {
|
||||
if (error) {
|
||||
if (friendlyErrorMessage) {
|
||||
error = utils.wrapError(error, friendlyErrorMessage);
|
||||
}
|
||||
return next(error);
|
||||
}
|
||||
req.oss.saveUserAlert(req, 'Thanks for processing the request with your ' + action.toUpperCase() + ' decision.', engine.typeName, 'success');
|
||||
if (action !== 'approve' || !engine.getApprovedViewName) {
|
||||
return res.redirect(req.teamUrl);
|
||||
}
|
||||
oss.render(req, res, engine.getApprovedViewName(), 'Approved', {
|
||||
pendingRequest: pendingRequest,
|
||||
results: output,
|
||||
team: team,
|
||||
teamUrl: req.teamUrl,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,232 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var utils = require('../../../utils');
|
||||
var approvalRoute = require('./approval/');
|
||||
|
||||
// Not a great place for these, should move into independent files eventually...
|
||||
|
||||
function PermissionWorkflowEngine (team, approvalPackage) {
|
||||
this.team = team;
|
||||
this.request = approvalPackage.request;
|
||||
this.user = approvalPackage.requestingUser;
|
||||
this.id = approvalPackage.id;
|
||||
this.typeName = 'Team Join';
|
||||
}
|
||||
|
||||
PermissionWorkflowEngine.prototype.messageForAction = function (action) {
|
||||
var message = null;
|
||||
if (action == 'deny') {
|
||||
message = 'This team join request has not been approved at this time.';
|
||||
} else if (action == 'approve') {
|
||||
message = 'Permission request approved.';
|
||||
}
|
||||
return message;
|
||||
};
|
||||
|
||||
PermissionWorkflowEngine.prototype.performApprovalOperation = function (callback) {
|
||||
var self = this;
|
||||
var team = self.team;
|
||||
team.addMembership('member', this.request.ghu, function (error) {
|
||||
if (error) {
|
||||
error = utils.wrapError(error, 'The GitHub API returned an error trying to add the user ' + this.request.ghu + ' to team ID ' + team.id +'.');
|
||||
}
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
// ---
|
||||
|
||||
function RepoWorkflowEngine (team, approvalPackage) {
|
||||
this.team = team;
|
||||
this.request = approvalPackage.request;
|
||||
this.user = approvalPackage.requestingUser;
|
||||
this.id = approvalPackage.id;
|
||||
this.typeName = 'Repository Create';
|
||||
}
|
||||
|
||||
RepoWorkflowEngine.prototype.messageForAction = function (action) {
|
||||
var message = null;
|
||||
if (action == 'deny') {
|
||||
message = 'The repo was not approved at this time.';
|
||||
} else if (action == 'approve') {
|
||||
message = 'The repo has been created.';
|
||||
}
|
||||
return message;
|
||||
};
|
||||
|
||||
RepoWorkflowEngine.prototype.editGet = function (req, res, next) {
|
||||
var self = this;
|
||||
var oss = self.team.oss;
|
||||
oss.render(req, res, 'org/team/approvals/editRepo', 'Edit Repo Request', {
|
||||
entry: this.request,
|
||||
teamUrl: req.teamUrl,
|
||||
team: req.team,
|
||||
});
|
||||
};
|
||||
|
||||
RepoWorkflowEngine.prototype.editPost = function (req, res, next) {
|
||||
var self = this;
|
||||
var dc = self.team.oss.dataClient();
|
||||
var visibility = req.body.repoVisibility;
|
||||
if (!(visibility == 'public' || visibility == 'private')) {
|
||||
return next(new Error('Visibility for the repo request must be provided.'));
|
||||
}
|
||||
var updates = {
|
||||
repoName: req.body.repoName,
|
||||
repoVisibility: visibility,
|
||||
repoUrl: req.body.repoUrl,
|
||||
repoDescription: req.body.repoDescription,
|
||||
};
|
||||
dc.updateApprovalRequest(self.id, updates, function (error) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'There was a problem updating the request.'));
|
||||
}
|
||||
res.redirect(req.teamUrl + 'approvals/' + self.id);
|
||||
});
|
||||
};
|
||||
|
||||
RepoWorkflowEngine.prototype.getApprovedViewName = function () {
|
||||
return 'org/team/repos/repoCreated';
|
||||
};
|
||||
|
||||
var createAddRepositoryTask = function createAddRepoTask(org, repoName, id, permission) {
|
||||
return function (cb) {
|
||||
org.team(id).addRepository(repoName, permission, function (error) {
|
||||
// Don't propagate as an error, just record the issue...
|
||||
var message = 'Successfully added the "' + repoName + '" repo to the team "' + id + '" with permission level ' + permission +'.';
|
||||
if (error) {
|
||||
message = 'The addition of the repo ' + repoName + ' to the team ' + id + ' could not be completed. The GitHub API returned an error.';
|
||||
}
|
||||
var result = {
|
||||
error: error,
|
||||
message: message,
|
||||
};
|
||||
cb(null, result);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
RepoWorkflowEngine.prototype.generateSecondaryTasks = function (callback) {
|
||||
var self = this;
|
||||
var pendingRequest = self.request;
|
||||
var tasks = [];
|
||||
var org = self.team.org;
|
||||
var repoName = pendingRequest.repoName;
|
||||
var teamsCount = Math.floor(pendingRequest.teamsCount);
|
||||
for (var i = 0; i < teamsCount; i++) {
|
||||
var key = 'teamid' + i;
|
||||
var teamId = pendingRequest[key];
|
||||
var permission = pendingRequest[key + 'p'];
|
||||
if (teamId && permission) {
|
||||
tasks.push(createAddRepositoryTask(org, repoName, teamId, permission));
|
||||
}
|
||||
}
|
||||
callback(null, tasks);
|
||||
};
|
||||
|
||||
RepoWorkflowEngine.prototype.performApprovalOperation = function (callback) {
|
||||
var self = this;
|
||||
var properties = {
|
||||
description: self.request.repoDescription,
|
||||
homepage: self.request.repoUrl,
|
||||
'private': self.request.repoVisibility == 'public' ? false : true,
|
||||
};
|
||||
var org = self.team.org;
|
||||
org.createRepository(self.request.repoName, properties, function (error) {
|
||||
if (error) {
|
||||
error = utils.wrapError(error, 'The GitHub API did not allow the creation of the new repo.');
|
||||
}
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
// ---
|
||||
|
||||
function createRequestEngine(team, approvalPackage, callback) {
|
||||
var engine = null;
|
||||
var rt = approvalPackage.request.type;
|
||||
switch (rt) {
|
||||
case 'repo':
|
||||
engine = new RepoWorkflowEngine(team, approvalPackage);
|
||||
break;
|
||||
default:
|
||||
case 'joinTeam':
|
||||
engine = new PermissionWorkflowEngine(team, approvalPackage);
|
||||
break;
|
||||
}
|
||||
if (!engine) {
|
||||
console.dir(approvalPackage.request);
|
||||
return callback(new Error('No request engine is supported for requests of type "' + rt + '".'));
|
||||
}
|
||||
callback(null, engine);
|
||||
}
|
||||
|
||||
// Find the request and assign the workflow engine
|
||||
|
||||
router.use(function (req, res, next) {
|
||||
req.oss.addBreadcrumb(req, 'Approvals');
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var team = req.team;
|
||||
team.getApprovals(function (error, approvals) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
req.oss.render(req, res, 'org/team/approvals', 'Approvals for ' + team.name, {
|
||||
team: team,
|
||||
pendingApprovals: approvals,
|
||||
teamUrl: req.teamUrl,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.use('/:requestid', function (req, res, next) {
|
||||
var team = req.team;
|
||||
var requestid = req.params.requestid;
|
||||
var oss = req.oss;
|
||||
var dc = req.app.settings.dataclient;
|
||||
dc.getApprovalRequest(requestid, function (error, pendingRequest) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'The pending request you are looking for does not seem to exist.'));
|
||||
}
|
||||
var userHash = {};
|
||||
userHash[pendingRequest.ghu] = pendingRequest.ghid;
|
||||
var requestingUser = null;
|
||||
oss.getCompleteUsersFromUsernameIdHash(userHash,
|
||||
function (error, users) {
|
||||
if (!error && !users[pendingRequest.ghu]) {
|
||||
error = new Error('Could not create an object to track the requesting user.');
|
||||
}
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
requestingUser = users[pendingRequest.ghu];
|
||||
var approvalPackage = {
|
||||
request: pendingRequest,
|
||||
requestingUser: requestingUser,
|
||||
id: requestid,
|
||||
};
|
||||
createRequestEngine(team, approvalPackage, function (error, engine) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
oss.addBreadcrumb(req, engine.typeName + ' Request');
|
||||
req.approvalEngine = engine;
|
||||
next();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Pass on to the context-specific routes.
|
||||
router.use('/:requestid', approvalRoute);
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,161 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var utils = require('../../../utils');
|
||||
|
||||
var teamReposRoute = require('./repos');
|
||||
var approvalsRoute = require('./approvals');
|
||||
var membersRoute = require('./members');
|
||||
var maintainersRoute = require('./maintainers');
|
||||
|
||||
// auth for maintainers and sudo admins only
|
||||
|
||||
router.use(function (req, res, next) {
|
||||
var team = req.team;
|
||||
var oss = team.oss;
|
||||
team.org.isUserSudoer(function (ignored, isAdmin) {
|
||||
// We look the team up first and THEN verify using admin
|
||||
// so that we don't scare users away with their sudo rights
|
||||
team.getOfficialMaintainers(function (error, maintainers) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
for (var i = 0; i < maintainers.length; i++) {
|
||||
if (maintainers[i].id == oss.id.github) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
if (isAdmin === true) {
|
||||
req.sudoMode = true;
|
||||
return next();
|
||||
}
|
||||
var err = new Error('You do not have permission to maintain this team.');
|
||||
err.detailed = "These aren't the droids you are looking for.";
|
||||
err.status = 403;
|
||||
err.fancyLink = {
|
||||
link: req.teamUrl + 'join',
|
||||
title: 'Request to join this team',
|
||||
};
|
||||
err.skipLog = true;
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var oss = req.oss;
|
||||
var team = req.team;
|
||||
var dc = oss.dataClient();
|
||||
async.parallel({
|
||||
employees: function (callback) {
|
||||
dc.getAllEmployees(callback);
|
||||
},
|
||||
maintainers: function (callback) {
|
||||
team.getMaintainers(function (error, maintainers) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
if (maintainers && maintainers.length && maintainers.length > 0) {
|
||||
oss.getLinksForUsers(maintainers, function (error) {
|
||||
async.each(maintainers, function (mt, cb) {
|
||||
mt.getDetailsByUsername(cb);
|
||||
}, function () {
|
||||
callback(error, maintainers);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
callback(null, []);
|
||||
}
|
||||
});
|
||||
},
|
||||
pendingApprovals: function (callback) {
|
||||
team.getApprovals(callback);
|
||||
},
|
||||
}, function (error, data) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
oss.render(req, res, 'org/team/index', team.name + ' in ' + team.org.name, {
|
||||
team: team,
|
||||
teamUrl: req.teamUrl,
|
||||
maintainers: data.maintainers,
|
||||
employees: data.employees,
|
||||
pendingApprovals: data.pendingApprovals,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/delete', function (req, res, next) {
|
||||
var team = req.team;
|
||||
team.delete(function (error) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
req.oss.saveUserAlert(req, 'Team deleted.', 'Delete', 'success');
|
||||
res.redirect(req.org ? req.org.baseUrl : '/');
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/delete', function (req, res, next) {
|
||||
var oss = req.oss;
|
||||
var team = req.team;
|
||||
oss.addBreadcrumb(req, 'Team Delete');
|
||||
oss.render(req, res, 'org/team/deleteTeamConfirmation', team.name + ' - Delete GitHub team', {
|
||||
team: team,
|
||||
teamUrl: req.teamUrl,
|
||||
});
|
||||
});
|
||||
|
||||
router.use('/repos', teamReposRoute);
|
||||
router.use('/approvals', approvalsRoute);
|
||||
router.use('/members', membersRoute);
|
||||
router.use('/maintainers', maintainersRoute);
|
||||
|
||||
router.get('/properties', function (req, res, next) {
|
||||
var oss = req.oss;
|
||||
var team = req.team;
|
||||
team.getDetails(function (error) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'Had trouble getting the detailed properties for this team.'));
|
||||
}
|
||||
var dc = oss.dataClient();
|
||||
dc.getAllEmployees(function (error, employees) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
oss.addBreadcrumb(req, 'Properties');
|
||||
oss.render(req, res, 'org/team/properties', team.name + ' - Properties', {
|
||||
team: team,
|
||||
employees: employees,
|
||||
teamUrl: req.teamUrl,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/properties', function (req, res, next) {
|
||||
var team = req.team;
|
||||
var oldName = team.name;
|
||||
var patchObject = {
|
||||
name: req.body.ghname,
|
||||
description: req.body.description,
|
||||
};
|
||||
team.update(patchObject, function (error) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
req.oss.saveUserAlert(req, 'Team properties updated on GitHub.', 'Properties Saved', 'success');
|
||||
var url = req.teamUrl;
|
||||
if (oldName !== patchObject.name) {
|
||||
url = team.org.baseUrl + 'teams/';
|
||||
}
|
||||
res.redirect(url);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,159 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var utils = require('../../../utils');
|
||||
|
||||
var teamMaintainerRoute = require('./index-maintainer');
|
||||
|
||||
router.get('/join', function (req, res, next) {
|
||||
var team = req.team;
|
||||
async.waterfall([
|
||||
function (callback) {
|
||||
team.isMember(callback);
|
||||
},
|
||||
function (isMember, callback) {
|
||||
if (isMember === true) {
|
||||
return next(utils.wrapError(null, 'You are already a member of the team ' + team.name, true));
|
||||
}
|
||||
team.org.queryUserMembership(false, function (error, result) {
|
||||
if (error) {
|
||||
result = false;
|
||||
}
|
||||
callback(null, result);
|
||||
});
|
||||
},
|
||||
], function (error, isOrgMember) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
if (isOrgMember && isOrgMember.state && isOrgMember.state == 'active') {
|
||||
team.getOfficialMaintainers(function (error, maintainers) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
req.oss.render(req, res, 'org/team/join', 'Join "' + team.name + '"', {
|
||||
team: team,
|
||||
teamMaintainers: maintainers,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
var err = new Error('You are not an active member of the organization. Please onboard/join first.');
|
||||
err.skipLog = true;
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/join', function (req, res, next) {
|
||||
var oss = req.oss;
|
||||
var org = req.org;
|
||||
var team = req.team;
|
||||
var justification = req.body.justification;
|
||||
if (justification === undefined || justification === '') {
|
||||
return next(utils.wrapError(null, 'You must include justification for your request.', true));
|
||||
}
|
||||
var notificationsRepo = org.getWorkflowRepository();
|
||||
var dc = oss.dataClient();
|
||||
var assignTo = null;
|
||||
var requestId = null;
|
||||
var allMaintainers = null;
|
||||
var issueNumber = null;
|
||||
async.waterfall([
|
||||
function (callback) {
|
||||
team.isMember(callback);
|
||||
},
|
||||
function (isMember, callback) {
|
||||
if (isMember === true) {
|
||||
return next(utils.wrapError(null, 'You are already a member of the team ' + team.name, true));
|
||||
}
|
||||
team.getOfficialMaintainers(callback);
|
||||
},
|
||||
function (maintainers, callback) {
|
||||
var approvalRequest = {
|
||||
ghu: oss.usernames.github,
|
||||
ghid: oss.id.github,
|
||||
justification: req.body.justification,
|
||||
requested: ((new Date()).getTime()).toString(),
|
||||
active: false,
|
||||
type: 'joinTeam',
|
||||
org: team.org.name,
|
||||
teamid: team.id,
|
||||
teamname: team.name,
|
||||
email: oss.modernUser().contactEmail(),
|
||||
name: oss.modernUser().contactName(),
|
||||
};
|
||||
var randomMaintainer = maintainers[Math.floor(Math.random() * maintainers.length)];
|
||||
if (!randomMaintainer.login) {
|
||||
return next(new Error('For some reason the randomly picked maintainer is not setup in the portal properly. Please report this bug.'));
|
||||
}
|
||||
assignTo = randomMaintainer.login;
|
||||
var mnt = [];
|
||||
for (var i = 0; i < maintainers.length; i++) {
|
||||
if (maintainers[i].login) {
|
||||
mnt.push('@' + maintainers[i].login);
|
||||
}
|
||||
}
|
||||
allMaintainers = mnt.join(', ');
|
||||
dc.insertApprovalRequest(team.id, approvalRequest, callback);
|
||||
},
|
||||
function (newRequestId) {
|
||||
requestId = newRequestId;
|
||||
var body = 'A team join request has been submitted by ' + oss.modernUser().contactName() + ' (' +
|
||||
oss.modernUser().contactEmail() + ', [' + oss.usernames.github + '](' +
|
||||
'https://github.com/' + oss.usernames.github + ')) to join your "' +
|
||||
team.name + '" team ' + 'in the "' + team.org.name + '" organization.' + '\n\n' +
|
||||
allMaintainers + ': Can a team maintainer [review this request now](' +
|
||||
'https://' + req.hostname + '/approvals/' + requestId + ')?\n\n' +
|
||||
'<em>If you use this issue to comment with the team maintainers, please understand that your comment will be visible by all members of the organization.</em>';
|
||||
var callback = arguments[arguments.length - 1];
|
||||
notificationsRepo.createIssue({
|
||||
title: 'Request to join team "' + team.org.name + '/' + team.name + '" by ' + oss.usernames.github,
|
||||
body: body,
|
||||
}, callback);
|
||||
},
|
||||
function (issue) {
|
||||
req.oss.saveUserAlert(req, 'Your request to join ' + team.name + ' has been submitted and will be reviewed by a team maintainer.', 'Permission Request', 'success');
|
||||
var callback = arguments[arguments.length - 1];
|
||||
if (issue.id && issue.number) {
|
||||
issueNumber = issue.number;
|
||||
dc.updateApprovalRequest(requestId, {
|
||||
issueid: issue.id.toString(),
|
||||
issue: issue.number.toString(),
|
||||
active: true
|
||||
}, callback);
|
||||
} else {
|
||||
callback(new Error('An issue could not be created. The response object representing the issue was malformed.'));
|
||||
}
|
||||
},
|
||||
function setAssignee () {
|
||||
var callback = arguments[arguments.length - 1];
|
||||
notificationsRepo.updateIssue(issueNumber, {
|
||||
assignee: assignTo,
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
// CONSIDER: Log. This error condition hits when a user has
|
||||
// been added to the org outside of the portal. Since they
|
||||
// are not associated with the workflow repo, they cannot
|
||||
// be assigned by GitHub - which throws a validation error.
|
||||
console.log('could not assign issue ' + issueNumber + ' to ' + assignTo);
|
||||
console.dir(error);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], function (error) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
res.redirect(team.org.baseUrl + 'approvals/' + requestId);
|
||||
});
|
||||
});
|
||||
|
||||
router.use(teamMaintainerRoute);
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,129 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var utils = require('../../../utils');
|
||||
|
||||
router.get('/:maintainerid/downgrade', function (req, res, next) {
|
||||
var team = req.team;
|
||||
var dc = req.app.settings.dataclient;
|
||||
var maintainerid = req.params.maintainerid;
|
||||
var oss = req.oss;
|
||||
dc.getLink(maintainerid, function (error, person) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
person = dc.reduceEntity(person);
|
||||
oss.addBreadcrumb(req, 'Downgrade ' + person.ghu);
|
||||
oss.render(req, res, 'org/team/maintainers/deleteConfirmation', team.name + ' - Downgrade user to a member from a maintainer?', {
|
||||
team: team,
|
||||
teamUrl: req.teamUrl,
|
||||
maintainer: person,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:maintainerid/downgrade', function (req, res, next) {
|
||||
var team = req.team;
|
||||
var dc = req.app.settings.dataclient;
|
||||
dc.getLink(req.params.maintainerid, function (error, link) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
var username = link.ghu._;
|
||||
team.addMembership('member', username, function (error, body) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
req.oss.saveUserAlert(req, 'Downgraded "' + username + '" to a standard user.', 'User permission downgraded', 'success');
|
||||
res.redirect(req.teamUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/add', function (req, res, next) {
|
||||
var team = req.team;
|
||||
var oss = req.oss;
|
||||
var id = req.body.maintainer2;
|
||||
var newMaintainer = oss.user(id);
|
||||
newMaintainer.getLinkRequired(function (error, link) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
if (link.ghu === undefined) {
|
||||
return next(new Error('No username.'));
|
||||
}
|
||||
team.addMembership('maintainer', link.ghu, function (error, updatedEntity) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
req.oss.saveUserAlert(req, 'Added "' + link.ghu + '" as a Team Maintainer. They now have the same permission level of access that you have.', 'Team Maintainer Added', 'success');
|
||||
res.redirect(req.teamUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/downgradeSelf', function (req, res, next) {
|
||||
var team = req.team;
|
||||
// NOTE: This path does not actually verify. You've been warned!
|
||||
// Remove the current user as a team maintainer.
|
||||
team.addMembership('member', req.oss.entities.link.ghu, function (error) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
req.oss.saveUserAlert(req, 'You\'ve downgraded yourself!', 'Dropping yourself as a team maintainer', 'success');
|
||||
res.redirect('/');
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/transfer', function (req, res, next) {
|
||||
var oss = req.oss;
|
||||
var team = req.team;
|
||||
var dc = req.app.settings.dataclient;
|
||||
dc.getAllEmployees(function (error, employees) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
oss.addBreadcrumb(req, 'Transfer my team maintainer role');
|
||||
oss.render(req, res, 'org/team/maintainers/transferConfirmation', team.name + ' - Transfer your team maintainance role', {
|
||||
team: team,
|
||||
teamUrl: req.teamUrl,
|
||||
employees: employees,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/transfer', function (req, res, next) {
|
||||
var team = req.team;
|
||||
var dc = req.app.settings.dataclient;
|
||||
var newMaintainer = req.body.newMaintainer;
|
||||
if (newMaintainer == req.user.github.id) {
|
||||
return next(new Error('You are already a team maintainer, so you cannot transfer the role to yourself.'));
|
||||
}
|
||||
dc.getLink(newMaintainer, function (error, link) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
var username = link.ghu._;
|
||||
team.addMembership('maintainer', username, function (error, body) {
|
||||
req.oss.saveUserAlert(req, 'Added "' + username + '" to the team as a maintainer.', 'Maintainer Transfer Part 1 of 2', 'success');
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
// Downgrade ourselves now!
|
||||
team.addMembership('member', function (error, body2) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
req.oss.saveUserAlert(req, 'Remove you as a maintainer.', 'Maintainer Transfer Part 2 of 2', 'success');
|
||||
res.redirect('/');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,177 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var utils = require('../../../utils');
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var team = req.team;
|
||||
var oss = req.oss;
|
||||
var dc = oss.dataClient();
|
||||
async.parallel({
|
||||
employees: function (callback) {
|
||||
dc.getAllEmployees(callback);
|
||||
},
|
||||
members: function (callback) {
|
||||
team.getMemberLinks(callback);
|
||||
},
|
||||
}, function (error, data) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
oss.addBreadcrumb(req, 'Members');
|
||||
oss.render(req, res, 'org/team/members', team.name + ' - Team Membership', {
|
||||
team: team,
|
||||
teamUrl: req.teamUrl,
|
||||
employees: data.employees,
|
||||
teamMembers: data.members,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/securityCheck', function (req, res, next) {
|
||||
var team = req.team;
|
||||
// This one is a little convoluted as the app has been refactored...
|
||||
var teamMembers = null;
|
||||
var usersNotInCompliance = [];
|
||||
async.waterfall([
|
||||
function (callback) {
|
||||
team.getDetails(callback);
|
||||
},
|
||||
function (details, callback) {
|
||||
team.getMemberLinks(callback);
|
||||
},
|
||||
function (memberLinks, callback) {
|
||||
teamMembers = memberLinks;
|
||||
// Now, get the org-wide audit list..
|
||||
team.org.getAuditList(callback);
|
||||
},
|
||||
function (naughtyUsers, callback) {
|
||||
for (var i = 0; i < teamMembers.length; i++) {
|
||||
var login = teamMembers[i].login;
|
||||
if (naughtyUsers[login]) {
|
||||
usersNotInCompliance.push(teamMembers[i]);
|
||||
}
|
||||
}
|
||||
callback(null, usersNotInCompliance);
|
||||
},
|
||||
], function (error, naughtyUsers) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
var oss = team.oss;
|
||||
oss.addBreadcrumb(req, 'Security Check');
|
||||
oss.render(req, res, 'org/team/securityCheck', team.name + ' - Team Security Check', {
|
||||
team: team,
|
||||
teamUrl: req.teamUrl,
|
||||
noncompliantUsers: naughtyUsers,
|
||||
});
|
||||
});
|
||||
});
|
||||
router.get('/:memberUsername/remove', function (req, res, next) {
|
||||
var team = req.team;
|
||||
var oss = req.oss;
|
||||
var dc = req.app.settings.dataclient;
|
||||
var removeUsername = req.params.memberUsername;
|
||||
if (!removeUsername || (removeUsername.length && removeUsername.length === 0)) {
|
||||
return next(new Error('A username must be provided.'));
|
||||
}
|
||||
// CONSIDER: NEED TO SUPPORT FOR ALL ORGANIZATIONS!
|
||||
oss.addBreadcrumb(req, 'Remove ' + removeUsername);
|
||||
dc.getUserLinkByUsername(removeUsername, function (error, link) {
|
||||
// Note: an error is ok here; if there is an error, we still show the page, as
|
||||
// this is likely a user added directly by a GitHub administrator for the
|
||||
// organization instead of through the portal/tooling. We want to make sure
|
||||
// the user will still be removable.
|
||||
oss.render(req, res, 'org/team/removeMemberConfirmation', team.name + ' - Remove User', {
|
||||
userInformation: dc.reduceEntity(link),
|
||||
removeUsername: removeUsername,
|
||||
team: team,
|
||||
teamUrl: req.teamUrl,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Remove a member by GitHub username (body field name: removeUsername)
|
||||
router.post('/:memberUsername/remove', function (req, res, next) {
|
||||
var removeUsername = req.params.memberUsername;
|
||||
var team = req.team;
|
||||
if (team.org === undefined) {
|
||||
return next(new Error('Org undefined.'));
|
||||
}
|
||||
var dc = req.app.settings.dataclient;
|
||||
if (!removeUsername || (removeUsername.length && removeUsername.length === 0)) {
|
||||
return next(new Error('A username must be provided.'));
|
||||
}
|
||||
dc.getUserLinkByUsername(removeUsername, function (error, link) {
|
||||
if (req.body.removeFromTeam !== undefined) {
|
||||
return team.removeMembership(removeUsername, function (error) {
|
||||
if (error) {
|
||||
return next(new Error('Removing the user from your team failed.'));
|
||||
}
|
||||
req.oss.saveUserAlert(req, removeUsername + ' has been removed from the team ' + team.name + '.', 'Team Member Remove', 'success');
|
||||
return res.redirect(req.teamUrl + 'members');
|
||||
});
|
||||
}
|
||||
// More intrusive all-org, plus link move...
|
||||
var org1 = team.org;
|
||||
var entity = null;
|
||||
if (link) {
|
||||
entity = dc.reduceEntity(link);
|
||||
}
|
||||
// CONSIDER: NEED TO SUPPORT FOR ALL ORGANIZATIONS!
|
||||
org1.removeUserMembership(removeUsername, function (error) {
|
||||
if (error) {
|
||||
return next(new Error('Removing the entire user failed. Please report this to the organization administrators to make sure the removal happens completely.'));
|
||||
}
|
||||
req.oss.saveUserAlert(req, removeUsername + ' has been removed from ' + org1.name + '.', 'Member Remove from Organization', 'success');
|
||||
if (entity && entity.ghu) {
|
||||
return dc.removeLink(entity.ghid, function (error, result, response) {
|
||||
if (error) {
|
||||
return next(new Error('Although the user was removed from the organization and is no longer able to access the site, a failure happened trying to remove the user from the portal system. If you could reach out to the administrators to hunt down this issue, that would be great. Thanks. Please include the GitHub username of the user, ' + removeUsername));
|
||||
}
|
||||
req.oss.saveUserAlert(req, removeUsername + ' has been removed from the corporate GitHub system.', 'Member Remove from the company', 'success');
|
||||
res.redirect(req.teamUrl + 'members');
|
||||
});
|
||||
}
|
||||
return res.redirect(req.teamUrl + 'members');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/add', function (req, res, next) {
|
||||
var team = req.team;
|
||||
var dc = req.app.settings.dataclient;
|
||||
var newMemberId = req.body.addMember;
|
||||
team.getMembersCached('all', function (error, members) {
|
||||
if (error) {
|
||||
return next(new Error('Team information not found.'));
|
||||
}
|
||||
for (var member in members) {
|
||||
var m = members[member];
|
||||
if (m.ghid && m.ghid == newMemberId) {
|
||||
return next(utils.wrapError(null,'This person is already a member of the team.', true));
|
||||
}
|
||||
}
|
||||
dc.getUserLinks([newMemberId], function (error, links) {
|
||||
if (!error && links && links.length > 0 && links[0] && links[0].ghu) {
|
||||
team.addMembership('member', links[0].ghu, function (error) {
|
||||
if (!error) {
|
||||
req.oss.saveUserAlert(req, 'Added "' + links[0].ghu + '" to the team.', 'Member Added', 'success');
|
||||
}
|
||||
return error ?
|
||||
next(new Error('The GitHub API returned an error, they may be under attack or currently having system problems. This tool is dependent on their system being available in real-time, sorry.')) :
|
||||
res.redirect(req.teamUrl + 'members');
|
||||
});
|
||||
} else {
|
||||
return next(new Error('We had trouble finding the official identity link information about this user. Please report this to the admins.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var repo = req.repo;
|
||||
var oss = repo.oss;
|
||||
oss.addBreadcrumb(req, 'Collaborators');
|
||||
repo.getOutsideCollaborators(function (error, outsideCollaborators, corporateCollaborators) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
var dc = oss.dataClient();
|
||||
dc.getAllEmployees(function (ignored, employees) {
|
||||
oss.render(req, res, 'org/team/repos/repo/collaborators', repo.name + ' Collaborators', {
|
||||
collaborators: outsideCollaborators,
|
||||
corporateCollaborators: corporateCollaborators,
|
||||
repoCollaboratorsUrl: req.teamReposUrl + repo.name + '/collaborators',
|
||||
employees: employees,
|
||||
repo: repo,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/add', function (req, res, next) {
|
||||
var repo = req.repo;
|
||||
var username = req.body.username;
|
||||
var permissionLevel = req.body.permission;
|
||||
var corporateCollaborator = req.body.corporate;
|
||||
if (!(permissionLevel == 'admin' || permissionLevel == 'push' || permissionLevel == 'pull')) {
|
||||
return next(new Error('Permission level "' + permissionLevel + '" not recognized.'));
|
||||
}
|
||||
var oss = repo.oss;
|
||||
repo.addCollaborator(username, permissionLevel, function (error) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
// CONSIDER: Audit log.
|
||||
var collaboratorType = corporateCollaborator ? 'Corporate Collaborator' : 'Outside Collaborator';
|
||||
oss.saveUserAlert(req, 'Added or updated ' + username, collaboratorType);
|
||||
res.redirect(req.teamReposUrl + repo.name + '/collaborators');
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:username/remove', function (req, res, next) {
|
||||
var repo = req.repo;
|
||||
var username = req.params.username;
|
||||
var oss = repo.oss;
|
||||
console.dir(username);
|
||||
repo.removeCollaborator(username, function (error) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
// CONSIDER: Audit log.
|
||||
oss.saveUserAlert(req, 'Removed ' + username, 'Collaborator Removed');
|
||||
res.redirect(req.teamReposUrl + repo.name + '/collaborators');
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,93 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var github = require('octonode');
|
||||
var moment = require('moment');
|
||||
var utils = require('../../../../utils');
|
||||
|
||||
var collaboratorsRoute = require('./collaborators');
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Repo rename, description and optional URL changes. Almost the same code path
|
||||
// as the "visibility swap" code.
|
||||
// ----------------------------------------------------------------------------
|
||||
router.get('/properties', function (req, res, next) {
|
||||
req.oss.addBreadcrumb(req, 'Properties');
|
||||
req.oss.render(req, res, 'org/team/repos/properties', req.repo.full_name + ' - Repository Properties', {
|
||||
team: req.team,
|
||||
repo: req.repo,
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/properties', function (req, res, next) {
|
||||
req.repo.update({
|
||||
name: req.body.name === '' ? undefined : req.body.name,
|
||||
homepage: req.body.homepage === '' ? undefined : req.body.homepage,
|
||||
description: req.body.description === '' ? undefined : req.body.description,
|
||||
}, function (error, result) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'There was a problem updating the properties for the repo. If you tried renaming the repo, was it legitimate? Please contact the admins.'));
|
||||
}
|
||||
res.redirect(req.teamReposUrl);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Swap visibility
|
||||
// ----------------------------------------------------------------------------
|
||||
router.get('/visibility/swap', function (req, res, next) {
|
||||
var team = req.team;
|
||||
var repo = req.repo;
|
||||
var oss = req.oss;
|
||||
oss.addBreadcrumb(req, 'Repository Visibility');
|
||||
oss.render(req, res, 'org/team/repos/goPublic', repo.full_name + ' - Visibility Settings', {
|
||||
team: team,
|
||||
repo: repo,
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/visibility/swap', function (req, res, next) {
|
||||
var repo = req.repo;
|
||||
repo.update({
|
||||
private: false,
|
||||
homepage: req.body.homepage === '' ? undefined : req.body.homepage,
|
||||
description: req.body.description === '' ? undefined : req.body.description,
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'There was a problem going public. Please contact the admins.'));
|
||||
}
|
||||
res.redirect(req.teamReposUrl);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Delete (destroy permanently) a repo
|
||||
// ----------------------------------------------------------------------------
|
||||
router.get('/delete', function (req, res, next) {
|
||||
var team = req.team;
|
||||
var repo = req.repo;
|
||||
req.oss.addBreadcrumb(req, 'Permanent Delete');
|
||||
req.oss.render(req, res, 'org/team/repos/delete', repo.full_name + ' - Delete', {
|
||||
team: team,
|
||||
repo: repo,
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/delete', function (req, res, next) {
|
||||
var repo = req.repo;
|
||||
repo.delete(function (error) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'There was a problem deleting the repo according to the GitHub API. Please contact the admins.'));
|
||||
}
|
||||
res.redirect(req.teamReposUrl);
|
||||
});
|
||||
});
|
||||
|
||||
router.use('/collaborators', collaboratorsRoute);
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var utils = require('../../../utils');
|
||||
var oneRepoRoute = require('./repo/');
|
||||
|
||||
router.use(function getTeamReposList(req, res, next) {
|
||||
var oss = req.oss;
|
||||
var team = req.team;
|
||||
req.teamReposUrl = req.teamUrl + 'repos/';
|
||||
oss.addBreadcrumb(req, 'Repositories');
|
||||
team.getRepos(function (error, repos) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
req.teamRepos = repos;
|
||||
next();
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var team = req.team;
|
||||
req.oss.render(req, res, 'org/team/repos', team.name + ' - Team Repos', {
|
||||
team: team,
|
||||
repos: req.teamRepos,
|
||||
teamUrl: req.teamUrl,
|
||||
});
|
||||
});
|
||||
|
||||
router.use('/:repoName/', function ensureOwnedTeam(req, res, next) {
|
||||
var repoName = req.params.repoName.toLowerCase();
|
||||
var repos = req.teamRepos;
|
||||
for (var i = 0; i < repos.length; i++) {
|
||||
if (repos[i] && repos[i].name && repos[i].name.toLowerCase() == repoName) {
|
||||
req.repo = repos[i];
|
||||
req.oss.addBreadcrumb(req, req.repo.name, false);
|
||||
return next();
|
||||
}
|
||||
}
|
||||
next(new Error('The repo "' + repoName + '" either does not exist or cannot be administered by this team.'));
|
||||
});
|
||||
|
||||
router.use('/:repoName/', oneRepoRoute);
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var utils = require('../../utils');
|
||||
|
||||
var teamRoute = require('./team/');
|
||||
|
||||
router.use(function (req, res, next) {
|
||||
req.org.oss.addBreadcrumb(req, 'Teams');
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var org = req.org;
|
||||
var onboardingOrJoining = req.query.joining || req.query.onboarding;
|
||||
async.parallel({
|
||||
allTeams: org.getTeams.bind(org),
|
||||
userTeams: org.getMyTeamMemberships.bind(org, 'all'),
|
||||
teamsMaintained: org.getMyTeamMemberships.bind(org, 'maintainer'),
|
||||
isAdministrator: function (callback) {
|
||||
org.isUserSudoer(function (ignored, isAdmin) {
|
||||
callback(null, isAdmin);
|
||||
});
|
||||
},
|
||||
orgUser: function (callback) {
|
||||
if (onboardingOrJoining) {
|
||||
org.getOrganizationUserProfile(callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
}, function (error, r) {
|
||||
var i = 0;
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
var userTeamsMaintainedById = {};
|
||||
var userIsMaintainer = false;
|
||||
if (r.teamsMaintained && r.teamsMaintained.length && r.teamsMaintained.length > 0) {
|
||||
userTeamsMaintainedById = utils.arrayToHashById(r.teamsMaintained);
|
||||
userIsMaintainer = true;
|
||||
}
|
||||
var userTeamsById = {};
|
||||
for (i = 0; i < r.userTeams.length; i++) {
|
||||
userTeamsById[r.userTeams[i].id] = true;
|
||||
}
|
||||
for (i = 0; i < r.allTeams.length; i++) {
|
||||
r.allTeams[i]._hack_isMember = userTeamsById[r.allTeams[i].id] ? true : false;
|
||||
}
|
||||
org.oss.render(req, res, 'org/teams', 'Join a team', {
|
||||
availableTeams: r.allTeams,
|
||||
highlightedTeams: org.getHighlightedTeams(),
|
||||
org: org,
|
||||
isSudoer: r.isAdministrator === true,
|
||||
onboardingOrJoining: onboardingOrJoining,
|
||||
orgUser: r.orgUser,
|
||||
userTeamsMaintainedById: userTeamsMaintainedById,
|
||||
userIsMaintainer: userIsMaintainer,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.use('/:teamname', function (req, res, next) {
|
||||
var org = req.org;
|
||||
var teamName = req.params.teamname;
|
||||
org.teamFromName(teamName, function (error, team) {
|
||||
if (error && error.slug) {
|
||||
return res.redirect(org.baseUrl + 'teams/' + error.slug);
|
||||
}
|
||||
if (!(team && team.id)) {
|
||||
if (!error) {
|
||||
error = new Error('No team named "' + teamName + "' could be found.");
|
||||
error.status = 404;
|
||||
} else {
|
||||
error = utils.wrapError('There was a problem querying for team information. The team may not exist.');
|
||||
}
|
||||
return next(error);
|
||||
}
|
||||
var teamId = team.id;
|
||||
var oss = org.oss;
|
||||
oss.getTeam(teamId, function (error, team) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
req.team = team;
|
||||
req.teamUrl = org.baseUrl + 'teams/' + team.slug + '/';
|
||||
req.org.oss.addBreadcrumb(req, team.name);
|
||||
next();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.use('/:teamname', teamRoute);
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,243 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var github = require('octonode');
|
||||
var moment = require('moment');
|
||||
|
||||
// These functions are not pretty.
|
||||
|
||||
router.use(function ensureOrganizationSudoer(req, res, next) {
|
||||
req.oss.isPortalAdministrator(function (error, isAdmin) {
|
||||
if (isAdmin === true) {
|
||||
return next();
|
||||
}
|
||||
next(new Error("These aren't the droids you are looking for. You do not have permission to be here."));
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
req.oss.render(req, res, 'organization/index', 'Organization Dashboard');
|
||||
});
|
||||
|
||||
function whoisById(dc, config, githubId, userInfo, callback) {
|
||||
dc.getLink(githubId, function (error, ok) {
|
||||
if (ok) {
|
||||
ok = dc.reduceEntity(ok);
|
||||
} else {
|
||||
ok = {
|
||||
githubInfoButNoLink: userInfo
|
||||
};
|
||||
}
|
||||
return callback(error, ok);
|
||||
});
|
||||
}
|
||||
|
||||
function expandAllInformation(req, dc, config, entity, callback) {
|
||||
var oss = req.oss;
|
||||
var orgsList = oss.orgs();
|
||||
var orgsUserIn = [];
|
||||
async.each(orgsList, function (org, callback) {
|
||||
org.queryAnyUserMembership(entity.ghu, function (err, membership) {
|
||||
if (membership && membership.state) {
|
||||
orgsUserIn.push(org);
|
||||
}
|
||||
callback(null, membership);
|
||||
});
|
||||
}, function (error) {
|
||||
entity.orgs = orgsUserIn;
|
||||
callback(null, entity);
|
||||
});
|
||||
// team memberships
|
||||
// org(s) memberships
|
||||
// "drop from org"
|
||||
// "drop from all orgs"
|
||||
// "email"
|
||||
}
|
||||
|
||||
router.get('/whois/aad/:upn', function (req, res, next) {
|
||||
var config = req.app.settings.runtimeConfig;
|
||||
var dc = req.app.settings.dataclient;
|
||||
var upn = req.params.upn;
|
||||
var oss = req.oss;
|
||||
dc.getUserByAadUpn(upn, function (error, usr) {
|
||||
if (error) {
|
||||
error.skipLog = true;
|
||||
return next(error);
|
||||
}
|
||||
if (usr.length && usr.length > 0) {
|
||||
expandAllInformation(req, dc, config, usr[0], function (error, z) {
|
||||
oss.render(req, res, 'organization/whois/result', 'Whois by AAD UPN: ' + upn, {
|
||||
info: z,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return next(new Error('User not found.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/errors/active', function (req, res, next) {
|
||||
var dc = req.app.settings.dataclient;
|
||||
var oss = req.oss;
|
||||
dc.getActiveErrors(function (error, errors) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
oss.render(req, res, 'organization/errorsList', 'Untriaged errors', {
|
||||
errors: errors,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/errors/:partition/:row', function (req, res, next) {
|
||||
var partitionKey = req.params.partition;
|
||||
var errorId = req.params.row;
|
||||
var action = req.body.action;
|
||||
var dc = req.app.settings.dataclient;
|
||||
if (action == 'Archive') {
|
||||
dc.updateError(partitionKey, errorId, {
|
||||
'new': false
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
req.oss.saveUserAlert(req, 'Error ' + partitionKey + '/' + errorId + ' troaged.', 'Marked as no longer a new error instance', 'success');
|
||||
res.redirect('/organization/errors/active/');
|
||||
});
|
||||
} else if (action == 'Delete') {
|
||||
dc.removeError(partitionKey, errorId, function (error) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
req.oss.saveUserAlert(req, 'Error ' + partitionKey + '/' + errorId + ' deleted.', 'Deleted', 'success');
|
||||
res.redirect('/organization/errors/active/');
|
||||
});
|
||||
} else {
|
||||
return next(new Error('Action not supported: ' + action));
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/whois/id/:githubid', function (req, res, next) {
|
||||
var config = req.app.settings.runtimeConfig;
|
||||
var dc = req.app.settings.dataclient;
|
||||
var id = req.params.githubid;
|
||||
var oss = req.oss;
|
||||
whoisById(dc, config, id, undefined, function (error, userInfoFinal) {
|
||||
expandAllInformation(req, dc, config, userInfoFinal, function (error, z) {
|
||||
oss.render(req, res, 'organization/whois/result', 'Whois by GitHub ID: ' + id, {
|
||||
info: z,
|
||||
postUrl: '/organization/whois/id/' + id,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/whois/id/:githubid', function (req, res, next) {
|
||||
var config = req.app.settings.runtimeConfig;
|
||||
var dc = req.app.settings.dataclient;
|
||||
var id = req.params.githubid;
|
||||
var valid = req.body['remove-link-only'];
|
||||
if (!valid) {
|
||||
return next(new Error('Invalid action for the ID POST action.'));
|
||||
}
|
||||
whoisById(dc, config, id, undefined, function (error, userInfoFinal) {
|
||||
expandAllInformation(req, dc, config, userInfoFinal, function (error, u) {
|
||||
var tasks = [];
|
||||
tasks.push(function removeLinkNow(callback) {
|
||||
dc.removeLink(id, callback);
|
||||
});
|
||||
async.series(tasks, function (error, results) {
|
||||
res.send('<pre>' + JSON.stringify({
|
||||
error: error,
|
||||
results: results
|
||||
}, undefined, 2) + '</pre>');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/whois/github/:username', function (req, res, next) {
|
||||
var config = req.app.settings.runtimeConfig;
|
||||
var dc = req.app.settings.dataclient;
|
||||
var username = req.params.username;
|
||||
var githubOrgClient = github.client(config.github.complianceToken);
|
||||
var ghuser = githubOrgClient.user(username);
|
||||
var oss = req.oss;
|
||||
ghuser.info(function (error, userInfo) {
|
||||
if (error) {
|
||||
error.skipLog = true;
|
||||
return next(error);
|
||||
}
|
||||
var id = userInfo.id;
|
||||
whoisById(dc, config, id, userInfo, function (error, userInfoFinal) {
|
||||
expandAllInformation(req, dc, config, userInfoFinal, function (error, z) {
|
||||
oss.render(req, res, 'organization/whois/result', 'Whois: ' + z.ghu, {
|
||||
info: z,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function generateRemoveMembershipFunction(dc, config, username, org) {
|
||||
var theOrg = org;
|
||||
return function (callback) {
|
||||
var o = theOrg;
|
||||
o.removeUserMembership(username, callback);
|
||||
};
|
||||
}
|
||||
|
||||
router.post('/whois/github/:username', function (req, res, next) {
|
||||
var config = req.app.settings.runtimeConfig;
|
||||
var dc = req.app.settings.dataclient;
|
||||
var username = req.params.username;
|
||||
var githubOrgClient = github.client(config.github.complianceToken);
|
||||
var ghuser = githubOrgClient.user(username);
|
||||
var valid = req.body['remove-all'] || req.body['remove-link-only'] || req.body['remove-primary-org'];
|
||||
if (!valid) {
|
||||
return next(new Error('Invalid action.'));
|
||||
}
|
||||
ghuser.info(function (error, userInfo) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
var id = userInfo.id;
|
||||
whoisById(dc, config, id, userInfo, function (error, userInfoFinal) {
|
||||
expandAllInformation(req, dc, config, userInfoFinal, function (error, u) {
|
||||
var removeAllOrgs = req.body['remove-all'];
|
||||
var removePrimaryOnly = req.body['remove-primary-org'];
|
||||
var removeLink = ! removePrimaryOnly; // Only if we know they have a link
|
||||
var tasks = [];
|
||||
if (removeAllOrgs && u.orgs && u.orgs.length > 0) {
|
||||
u.orgs.reverse(); // want to end with the primary organization
|
||||
for (var i = 0; i < u.orgs.length; i++) {
|
||||
var org = u.orgs[i];
|
||||
tasks.push(generateRemoveMembershipFunction(dc, config, username, org));
|
||||
}
|
||||
} else if (removePrimaryOnly) {
|
||||
// When there is no link... edge case.
|
||||
// EDGE CASE: This may need an update.
|
||||
tasks.push(generateRemoveMembershipFunction(dc, config, username, config.github.organization));
|
||||
}
|
||||
if (removeLink) {
|
||||
tasks.push(function removeLinkNow(callback) {
|
||||
dc.removeLink(id, callback);
|
||||
});
|
||||
}
|
||||
async.series(tasks, function (error, results) {
|
||||
res.send('<pre>' + JSON.stringify({
|
||||
error: error,
|
||||
results: results
|
||||
}, undefined, 2) + '</pre>');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
|
||||
var orgRoute = require('./org/');
|
||||
|
||||
router.use('/:orgName', function (req, res, next) {
|
||||
var oss = req.oss;
|
||||
var orgName = req.params.orgName;
|
||||
try {
|
||||
req.org = oss.org(orgName);
|
||||
next();
|
||||
} catch (ex) {
|
||||
if (orgName.toLowerCase() == 'account') {
|
||||
return res.redirect('/');
|
||||
}
|
||||
var err = new Error('Organization Not Found');
|
||||
err.status = 404;
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.use('/:orgName', orgRoute);
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
|
||||
var cachedPackageInformation = null;
|
||||
|
||||
// Super-synchronous but rarely used page...
|
||||
function getPackageInfo() {
|
||||
if (cachedPackageInformation) {
|
||||
return cachedPackageInformation;
|
||||
}
|
||||
var thisPackage = require('../package.json');
|
||||
cachedPackageInformation = {};
|
||||
for (var dependency in thisPackage.dependencies) {
|
||||
var componentPackage = require('../node_modules/' + dependency + '/package.json');
|
||||
if (componentPackage && componentPackage.homepage) {
|
||||
cachedPackageInformation[dependency] = {
|
||||
homepage: componentPackage.homepage,
|
||||
description: componentPackage.description,
|
||||
};
|
||||
}
|
||||
}
|
||||
return cachedPackageInformation;
|
||||
}
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var config = req.app.settings.runtimeConfig;
|
||||
var components = getPackageInfo();
|
||||
res.render('thanks', {
|
||||
user: req.user,
|
||||
config: config,
|
||||
components: components,
|
||||
serviceBanner: config && config.serviceBanner ? config.serviceBanner : undefined,
|
||||
title: 'Open Source Portal for GitHub - ' + config.companyName});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,68 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var async = require('async');
|
||||
var utils = require('../utils');
|
||||
|
||||
router.use(function (req, res, next) {
|
||||
var oss = req.oss;
|
||||
var memberOfOrgs = [];
|
||||
async.each(oss.orgs(), function (o, callback) {
|
||||
o.queryUserMembership(false /* no caching */, function (error, result) {
|
||||
var state = null;
|
||||
if (result && result.state) {
|
||||
state = result.state;
|
||||
}
|
||||
if (state == 'active' || state == 'pending') {
|
||||
memberOfOrgs.push(o);
|
||||
}
|
||||
callback(error);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
req.currentOrganizationMemberships = memberOfOrgs;
|
||||
next();
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
var link = req.oss.entities.link;
|
||||
if (link && link.ghu) {
|
||||
return req.oss.render(req, res, 'unlink', 'Remove corporate link and organization memberships', {
|
||||
orgs: req.currentOrganizationMemberships,
|
||||
});
|
||||
} else {
|
||||
return next('No link could be found.');
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', function (req, res, next) {
|
||||
var currentOrganizationMemberships = req.currentOrganizationMemberships;
|
||||
async.each(currentOrganizationMemberships, function (org, callback) {
|
||||
org.removeUserMembership(function () {
|
||||
// CHANGE: We now continue with deletes when one fails. Common
|
||||
// failure case is when they have a pending invite, it will live
|
||||
// on... which is not ideal.
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
var dc = req.app.settings.dataclient;
|
||||
var oss = req.oss;
|
||||
dc.removeLink(oss.id.github, function (error) {
|
||||
if (error) {
|
||||
return next(utils.wrapError(error, 'You were successfully removed from all of your organizations. However, a minor failure happened during a data housecleaning operation. Double check that you are happy with your current membership status on GitHub.com before continuing. Press Report Bug if you would like this handled for sure.'));
|
||||
}
|
||||
delete req.user.azure;
|
||||
req.logout();
|
||||
res.redirect('/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,185 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
var async = require('async');
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Returns an integer, random, between low and high (exclusive) - [low, high)
|
||||
// ----------------------------------------------------------------------------
|
||||
exports.randomInteger = function (low, high) {
|
||||
return Math.floor(Math.random() * (high - low) + low);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Provide our own error wrapper and message for an underlying thrown error.
|
||||
// Useful for the user-presentable version.
|
||||
// ----------------------------------------------------------------------------
|
||||
exports.wrapError = function (error, message, userIntendedMessage) {
|
||||
var err = new Error(message);
|
||||
err.innerError = error;
|
||||
if (error && error.stack) {
|
||||
err.stack = error.stack;
|
||||
}
|
||||
if (userIntendedMessage === true) {
|
||||
err.skipLog = true;
|
||||
}
|
||||
return err;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Split and set an optional array, or empty array, trimming each.
|
||||
// ----------------------------------------------------------------------------
|
||||
exports.arrayFromString = function (a, split) {
|
||||
if (!split) {
|
||||
split = ',';
|
||||
}
|
||||
var b = a && a.split ? a.split(split) : [];
|
||||
if (b && b.length) {
|
||||
for (var i = 0; i < b.length; i++) {
|
||||
b[i] = b[i].trim();
|
||||
}
|
||||
}
|
||||
return b;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Simplistic merge of setting properties from b on object a.
|
||||
// ----------------------------------------------------------------------------
|
||||
exports.merge = function (a, b) {
|
||||
if (a && b) {
|
||||
for (var key in b) {
|
||||
a[key] = b[key];
|
||||
}
|
||||
}
|
||||
return a;
|
||||
};
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Improved "Is Array" check.
|
||||
// ----------------------------------------------------------------------------
|
||||
exports.isArray = function (value) {
|
||||
return value && typeof value === 'object' && value.constructor === Array;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Retrieves all pages of a GitHub (octonode) API endpoint by following the
|
||||
// next link, if present, in results. Each page is of max GitHub-allowed size,
|
||||
// 100 items. Keep in mind that each page is 1 API call from the API allownace.
|
||||
// ----------------------------------------------------------------------------
|
||||
exports.retrieveAllPages = function retrieveAllPages(method, optionalFilter, callback) {
|
||||
if (typeof optionalFilter == 'function') {
|
||||
callback = optionalFilter;
|
||||
optionalFilter = null;
|
||||
}
|
||||
var done = false;
|
||||
var page = 1;
|
||||
var results = [];
|
||||
async.whilst(
|
||||
function () { return !done; },
|
||||
function (cb) {
|
||||
var params = {
|
||||
page: page++,
|
||||
per_page: 100,
|
||||
};
|
||||
if (optionalFilter) {
|
||||
exports.merge(params, optionalFilter);
|
||||
}
|
||||
method.call(null, params, function (error, result, headers) {
|
||||
if (error) {
|
||||
done = true;
|
||||
} else {
|
||||
if (result && result.length) {
|
||||
results = results.concat(result);
|
||||
}
|
||||
done = !(headers && headers.link && headers.link.indexOf('rel="next"') >= 0);
|
||||
}
|
||||
cb(error);
|
||||
});
|
||||
},
|
||||
function (error) {
|
||||
callback(error, error ? undefined : results);
|
||||
});
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// A destructive removal function for an object. Removes a single key.
|
||||
// ----------------------------------------------------------------------------
|
||||
exports.stealValue = function steal(obj, key) {
|
||||
if (obj[key] !== undefined) {
|
||||
var val = obj[key];
|
||||
delete obj[key];
|
||||
return val;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Given a list of string values, check a string, using a case-insensitive
|
||||
// comparison.
|
||||
// ----------------------------------------------------------------------------
|
||||
exports.inListInsensitive = function ili(list, value) {
|
||||
value = value.toLowerCase();
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i].toLowerCase() === value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Given a list of lowercase values, check whether a value is present.
|
||||
// ----------------------------------------------------------------------------
|
||||
exports.isInListAnycaseInLowercaseList = function iila(list, value) {
|
||||
value = value.toLowerCase();
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i] === value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Given an array of things that have an `id` property, return a hash indexed
|
||||
// by that ID.
|
||||
// ----------------------------------------------------------------------------
|
||||
exports.arrayToHashById = function athi(inputArray) {
|
||||
var hash = {};
|
||||
if (inputArray && inputArray.length) {
|
||||
for (var i = 0; i < inputArray.length; i++) {
|
||||
if (inputArray[i] && inputArray[i].id) {
|
||||
hash[inputArray[i].id] = inputArray[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// A very basic breadcrumb stack that ties in to an Express request object.
|
||||
// ----------------------------------------------------------------------------
|
||||
exports.addBreadcrumb = function (req, breadcrumbTitle, optionalBreadcrumbLink) {
|
||||
if (req === undefined || req.baseUrl === undefined) {
|
||||
throw new Error('addBreadcrumb: did you forget to provide a request object instance?');
|
||||
}
|
||||
if (!optionalBreadcrumbLink && optionalBreadcrumbLink !== false) {
|
||||
optionalBreadcrumbLink = req.baseUrl;
|
||||
}
|
||||
if (!optionalBreadcrumbLink && optionalBreadcrumbLink !== false) {
|
||||
optionalBreadcrumbLink = '/';
|
||||
}
|
||||
var breadcrumbs = req.breadcrumbs;
|
||||
if (breadcrumbs === undefined) {
|
||||
breadcrumbs = [];
|
||||
}
|
||||
breadcrumbs.push({
|
||||
title: breadcrumbTitle,
|
||||
url: optionalBreadcrumbLink,
|
||||
});
|
||||
req.breadcrumbs = breadcrumbs;
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
//-
|
||||
//- Copyright (c) Microsoft. All rights reserved.
|
||||
//- Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//-
|
||||
|
||||
extends layout
|
||||
|
||||
block content
|
||||
div.container#top(style='margin-top:60px')
|
||||
div.container#content
|
||||
div.row
|
||||
div.col-md-4
|
||||
p
|
||||
img(src='/img/rainycloud.png', style='border:0; width:318px; height:318px', title="It's a rainy day. This is an error page.")
|
||||
|
||||
div.col-md-8
|
||||
p
|
||||
em Oops. It's a little rainy in the cloud today.
|
||||
|
||||
h1= message
|
||||
if errorStatus
|
||||
h2= 'HTTP ' + errorStatus
|
||||
|
||||
if detailed
|
||||
p.lead= detailed
|
||||
|
||||
if errorFancyLink
|
||||
p
|
||||
a.btn.btn-primary(href=errorFancyLink.link)= errorFancyLink.title
|
||||
|
||||
if config && config.corporate && config.corporate.portalAdministratorEmail
|
||||
p
|
||||
if correlationId && user && user.github && user.github.username
|
||||
a.btn.btn-sm.btn-muted(href='mailto:' + config.corporate.portalAdministratorEmail + '?subject=Open Source Portal Error Message ' + (message ? ': ' + message : '') + '&body=I ran into an error message while using the open source portal.%0D%0A%0D%0AThe error included a Correlation ID: ' + correlationId + '%0D%0A%0D%0ATo speed this request up, could you also share information about what you were trying to do at the time of the error? Thanks.%0D%0A%0D%0A---%0D%0AGitHub username: ' + user.github.username + (message ? '%0D%0AMessage: ' + message : '') + (error.status ? '%0D%0AStatus Code: ' + error.status : '') + (detailed ? '%0D%0ADetailed Message: ' + detailed : ''))
|
||||
| Report bug
|
||||
else if correlationId
|
||||
a.btn.btn-sm.btn-muted(href='mailto:' + config.corporate.portalAdministratorEmail + '?subject=Open Source Portal Error Message&body=I ran into an error message while using the open source portal.%0D%0A%0D%0AThe error included a Correlation ID: ' + correlationId + '%0D%0A%0D%0ATo speed this request up, could you also share information about your GitHub username and what you were trying to do at the time of the error? Thanks.')
|
||||
| Report bug
|
||||
else
|
||||
a.btn.btn-sm.btn-muted(href='mailto:' + config.corporate.portalAdministratorEmail + '?subject=Open Source Portal Error Message&body=I ran into an error message while using the open source portal.%0D%0A%0D%0AThe error message did not include a Correlation ID.')
|
||||
| Report bug
|
||||
|
||||
if correlationId
|
||||
small
|
||||
| Correlation ID: #{correlationId}
|
|
@ -0,0 +1,33 @@
|
|||
//-
|
||||
//- Copyright (c) Microsoft. All rights reserved.
|
||||
//- Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//-
|
||||
|
||||
extends layout
|
||||
|
||||
block content
|
||||
div.container
|
||||
|
||||
if user
|
||||
|
||||
h1= user.github.displayName || user.github.username
|
||||
|
||||
else
|
||||
|
||||
h1.huge
|
||||
| Open Source Portal
|
||||
small for GitHub
|
||||
|
||||
p.lead Self-service GitHub organization management portal for #{config.companyName}
|
||||
|
||||
p This portal empowers #{config.companyName} employees to perform self-service GitHub organization tasks including org onboarding and team membership workflows for several #{config.companyName} orgs. This portal is intended for employee use.
|
||||
|
||||
if corporateLinks && corporateLinks.length && corporateLinks.length > 0
|
||||
h2 #{config.companyName} Open Source
|
||||
ul
|
||||
each cl in corporateLinks
|
||||
li
|
||||
a(href=cl.link)
|
||||
= cl.title
|
||||
|
|
||||
i.glyphicon.glyphicon-share-alt
|
|
@ -0,0 +1,100 @@
|
|||
//-
|
||||
//- Copyright (c) Microsoft. All rights reserved.
|
||||
//- Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//-
|
||||
|
||||
extends layout
|
||||
|
||||
block js_doc_ready
|
||||
| try {
|
||||
| if (initializeManageFilter !== undefined) { initializeManageFilter() }
|
||||
| if (initializeMembershipFilter !== undefined) { initializeMembershipFilter() }
|
||||
| } catch(ex) {};
|
||||
|
||||
block content
|
||||
div.container
|
||||
if onboarding === true
|
||||
h1.huge Hi.
|
||||
else
|
||||
h1
|
||||
| Open Source Portal
|
||||
small for GitHub
|
||||
if accountInfo && onboarding !== true
|
||||
div.container
|
||||
div.row(style='margin-top:16px')
|
||||
// linked membership
|
||||
div.col-md-4.col-lg-4.col-sm-4
|
||||
if accountInfo.isLinkedUser !== false
|
||||
div.metro-box.ms-blue
|
||||
a(href='/unlink')
|
||||
h3 Linked Identity
|
||||
p= config.companyName.toUpperCase()
|
||||
else
|
||||
div.metro-box.ms-light-gray
|
||||
a(href='/link')
|
||||
h3 Not linked
|
||||
p= config.companyName.toUpperCase()
|
||||
// 2-factor authentication
|
||||
if accountInfo.twoFactorOff === true || accountInfo.twoFactorOn === true
|
||||
div.col-md-4.col-lg-4.col-sm-4
|
||||
if accountInfo.twoFactorOff === true
|
||||
div.metro-box.ms-red
|
||||
a(href=activeOrgUrl + 'security-check')
|
||||
h3 2FA
|
||||
p OFF
|
||||
else if accountInfo.twoFactorOn === true
|
||||
div.metro-box.ms-green
|
||||
a(href=activeOrgUrl + 'security-check')
|
||||
h3 2-factor auth
|
||||
p PROTECTED
|
||||
div.row(style='margin-top:16px')
|
||||
div.col-md-4.col-lg-4.col-sm-4
|
||||
if accountInfo.isLinkedUser !== false
|
||||
p You've linked your GitHub and corporate identities.
|
||||
else
|
||||
p To join the organization, you need to <a href="/link">link your GitHub and Microsoft-related identities</a>.
|
||||
div.col-md-4.col-lg-4.col-sm-4
|
||||
if accountInfo.twoFactorOff === true || accountInfo.twoFactorOn === true
|
||||
if accountInfo.twoFactorOff === true
|
||||
p Your account is a security risk and is not in compliance. <strong>Your access may be revoked at any time, without notice, and may cause work stoppage or loss.</strong>
|
||||
else if accountInfo.twoFactorOn === true
|
||||
p Your account uses modern security. Thank you.
|
||||
|
||||
if accountInfo.organizations
|
||||
if onboarding !== true
|
||||
h1
|
||||
a.a-unstyled(name='orgs') #{config.companyName} Organizations
|
||||
|
||||
if onboarding === true
|
||||
h3 Please select the organization that you would like to join.
|
||||
|
||||
each o in accountInfo.organizations
|
||||
div.link-box
|
||||
a(href=o.baseUrl + (o.membershipStateTemporary === 'active' ? '' : 'join') + onboardingPostfixUrl)
|
||||
h2
|
||||
strong.capitalize= o.name
|
||||
|
|
||||
if o.membershipStateTemporary == 'active'
|
||||
small Member
|
||||
else if o.membershipStateTemporary == 'pending'
|
||||
small
|
||||
span.label.label-danger Membership Pending
|
||||
else
|
||||
small
|
||||
span.label.label-primary Join this organization
|
||||
p.lead= o.setting('description')
|
||||
|
||||
//-if accountInfo.isSudoer
|
||||
h1 Organization Administration
|
||||
p Your account is a delegate administrator for the organization. You have additional capabilities enabled to help ensure the health of the organization.
|
||||
p
|
||||
a.btn.btn-default(href='/organization') Organization Delegate Dashboard
|
||||
|
||||
if accountInfo.teamsMaintained && accountInfo.teamsMaintained.length && accountInfo.teamsMaintained.length > 0
|
||||
if accountInfo.pendingApprovals && accountInfo.pendingApprovals.length && accountInfo.pendingApprovals.length > 0
|
||||
h1 Approvals: Please Review
|
||||
p
|
||||
a.btn.btn-default(href='/approvals/') See all pending approvals (#{accountInfo.pendingApprovals.length})
|
||||
hr
|
||||
p
|
||||
a.btn.btn-default(href='/unlink') Remove my corporate GitHub access
|
|
@ -0,0 +1,175 @@
|
|||
//-
|
||||
//- Copyright (c) Microsoft. All rights reserved.
|
||||
//- Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//-
|
||||
|
||||
doctype html
|
||||
html(lang="en")
|
||||
head
|
||||
meta(charset='utf-8')
|
||||
title= (user && user.github && user.github.username) ? title + ' - ' + user.github.username : title
|
||||
meta(http-equiv='X-UA-Compatible', content='IE=edge')
|
||||
meta(name='viewport', content='width=device-width, initial-scale=1.0')
|
||||
meta(name='author', content='Azure Team')
|
||||
link(href='/css/bootstrap.min.css?1', rel='stylesheet')
|
||||
link(href='/css/oss.css?1e', rel='stylesheet')
|
||||
link(rel='shortcut icon', href='/favicon.ico')
|
||||
link(rel='apple-touch-icon', sizes='114x114,72x72,144x144,60x60,120x120,76x76,152x152,180x180', href='/favicon-144.png')
|
||||
meta(name='msapplication-config', content='none')
|
||||
//[if lt IE 9]
|
||||
<script src="https://ajax.aspnetcdn.com/ajax/respond/1.4.2/respond.min.js"/>
|
||||
[endif]
|
||||
script(type='text/javascript', src='/js/jquery.min.js')
|
||||
script(type='text/javascript', src='/js/bootstrap.min.js')
|
||||
script(type='text/javascript', src='/js/timeago.js')
|
||||
script(type='text/javascript', src='/js/jquery.uitablefilter.js')
|
||||
| <script type='text/javascript'>
|
||||
| $(document).ready(function() {
|
||||
block js_doc_ready
|
||||
| jQuery('time').timeago();
|
||||
if alerts
|
||||
each alert in alerts
|
||||
| setTimeout(function () {$('#layout-alert-#{alert.number}').alert('close');}, 8000 * #{alert.number});
|
||||
| });
|
||||
| </script>
|
||||
body
|
||||
if alerts || serviceBanner || sudoMode
|
||||
div.alerts
|
||||
if sudoMode
|
||||
div.alert(id='sudo-banner', class='alert-danger')
|
||||
div.container
|
||||
h3 Organization Sudoer
|
||||
p You are currently authorized on this page via your sudo capabilities as an organization administrator.
|
||||
if serviceBanner
|
||||
div.alert(id='service-banner', class='alert-info', role='alert')
|
||||
button.close(type='button', data-dismiss='alert' aria-label='Close')
|
||||
span(aria-hidden='true') ×
|
||||
div.container
|
||||
h3 Service Alert
|
||||
h4= serviceBanner
|
||||
if alerts
|
||||
each alert in alerts
|
||||
div.alert(id='layout-alert-' + alert.number, class='alert-' + (alert.context ? alert.context : 'info'), role='alert')
|
||||
button.close(type='button', data-dismiss='alert' aria-label='Close')
|
||||
span(aria-hidden='true') ×
|
||||
div.container
|
||||
if alert.title
|
||||
h3= alert.title
|
||||
h4= alert.message
|
||||
if alert.optionalLink
|
||||
p
|
||||
a.btn.btn-muted(href=alert.optionalLink, title=alert.optionalLink)= alert.optionalCaption ? alert.optionalCaption : alert.optionalLink
|
||||
div.navbar.navbar-default.second-row-nav
|
||||
div.container
|
||||
div.navbar-header
|
||||
//-button.navbar-toggle.collapsed(type='button', data-toggle='collapse', data-target='.nav-collapse')
|
||||
//-button.navbar-toggle(type='button', data-toggle='collapse', data-target='.nav-collapse')
|
||||
span.sr-only Toggle navigation
|
||||
span.icon-bar
|
||||
span.icon-bar
|
||||
span.icon-bar
|
||||
//- a.navbar-brand(href='./')= page.folderMetadata.title
|
||||
//nav.collapse.navbar-collapse.nav-collapse(role='navigation')
|
||||
nav(role='navigation')
|
||||
div.container(style='margin-top:24px;margin-bottom:12px')
|
||||
div.row(style=(user && !error && ossLink) ? 'margin-left:0' : 'margin-left:-30px')
|
||||
div.col-md-6
|
||||
if user && user.github
|
||||
h4 Your GitHub Account
|
||||
else
|
||||
h4 GitHub Account
|
||||
if user && user.github
|
||||
p
|
||||
if user.github && user.github.avatarUrl
|
||||
img(alt=user.github.displayName, src=user.github.avatarUrl + '&s=80', style='margin-right:10px;width:30px;height:30px', data-user=user.github.id)
|
||||
a.btn.btn-sm.btn-muted(href='https://github.com/settings/profile', target='_new', title='Click to edit your public GitHub profile')= user.github.username
|
||||
a.btn.btn-sm.btn-muted-more(href='https://github.com/settings/profile', target='_new', title='Click to edit your public GitHub profile')= user.github.displayName || user.github.username
|
||||
a.btn.btn-sm.btn-white(href='/signout', style='margin-left:10px') Sign out
|
||||
else
|
||||
p
|
||||
small Sign in or create your GitHub.com account to manage your #{config.companyName} open source identity.
|
||||
p
|
||||
a.btn.btn-primary(href='/signin/github') Sign in
|
||||
div.col-md-6
|
||||
if user && !error
|
||||
if ossLink
|
||||
h4 Your #{config && config.companyName ? config.companyName : 'Corporate'} Identity
|
||||
p
|
||||
if ossLink.aadname
|
||||
a.btn.btn-sm.btn-muted(href='/signin/azure')= ossLink.aadname
|
||||
a.btn.btn-sm.btn-muted-more(href='/signin/azure')= ossLink.aadupn
|
||||
a.btn.btn-sm.btn-white(href='/link/update', style='margin-left:10px') Change
|
||||
else if user.azure
|
||||
//- NOTE: This is actually visually backward from the above link display...
|
||||
h4 Your #{config && config.companyName ? config.companyName : 'Corporate'} Identity
|
||||
p
|
||||
if user.azure.username
|
||||
a.btn.btn-sm.btn-muted(href='/signout/azure')= user.azure.username
|
||||
if user.azure.displayName
|
||||
a.btn.btn-sm.btn-muted-more(href='/signout/azure')= user.azure.displayName
|
||||
a.btn.btn-sm.btn-white(href='/signout/azure', style='margin-left:10px') Sign Out
|
||||
//- Just show breadcrumbs when there is an interesting path available
|
||||
if showBreadcrumbs === true && breadcrumbs && breadcrumbs.length && breadcrumbs.length > 1
|
||||
div.container
|
||||
ol.breadcrumb
|
||||
each crumb in breadcrumbs
|
||||
li
|
||||
if crumb.isLast === true || crumb.url === false
|
||||
span.capitalize= crumb.title
|
||||
else
|
||||
a.capitalize(href=crumb.url)= crumb.title
|
||||
|
||||
// content
|
||||
block content
|
||||
// end of content
|
||||
|
||||
footer.wiki-footer
|
||||
hr
|
||||
div.container
|
||||
div
|
||||
p(class='pull-right')
|
||||
a(href='#top', title=headSha) Back to top
|
||||
if ! user
|
||||
| ·
|
||||
a(href='/signin/github') Sign In
|
||||
|
||||
if user && user.github && user.github.id
|
||||
if config && config.corporate && config.corporate.trainingResources
|
||||
- var footres = config.corporate.trainingResources.footer
|
||||
if footres
|
||||
div.clearfix
|
||||
div.row(style='margin-bottom:24px')
|
||||
each categoryList, category in footres
|
||||
div.col-md-3.col-lg-3
|
||||
h5= category
|
||||
ul.list-unstyled
|
||||
each item in categoryList
|
||||
li
|
||||
a(href=item.link)= item.title
|
||||
div.clearfix
|
||||
p
|
||||
small It is important to note that uptime of this service is dependent on GitHub's API availability and rate limit.
|
||||
ul.list-inline
|
||||
if config && config.corporate && config.corporate.portalAdministratorEmail
|
||||
li
|
||||
a(href='mailto:' + config.corporate.portalAdministratorEmail) Contact Portal Administrator
|
||||
li
|
||||
a(href='https://github.com/azure/azure-oss-portal', target='_new') Contribute to this portal on GitHub
|
||||
div
|
||||
p
|
||||
small
|
||||
| © #{config && config.companyName ? config.companyName : ''}
|
||||
br
|
||||
if serverName && correlationId
|
||||
| Powered by
|
||||
span(title=correlationId + ' ' + serverName) Microsoft Azure,
|
||||
a(href='/thanks') great open source
|
||||
| and the GitHub API
|
||||
else
|
||||
| Powered by Microsoft Azure,
|
||||
a(href='/thanks') great open source
|
||||
| and the GitHub API
|
||||
|
||||
if (appInsightsKey)
|
||||
script(type='text/javascript').
|
||||
var appInsights=window.appInsights||function(config){function s(config){t[config]=function(){var i=arguments;t.queue.push(function(){t[config].apply(t,i)})}}var t={config:config},r=document,f=window,e="script",o=r.createElement(e),i,u;for(o.src=config.url||"//az416426.vo.msecnd.net/scripts/a/ai.0.js",r.getElementsByTagName(e)[0].parentNode.appendChild(o),t.cookie=r.cookie,t.queue=[],i=["Event","Exception","Metric","PageView","Trace"];i.length;)s("track"+i.pop());return config.disableExceptionTracking||(i="onerror",s("_"+i),u=f[i],f[i]=function(config,r,f,e,o){var s=u&&u(config,r,f,e,o);return s!==!0&&t["_"+i](config,r,f,e,o),s}),t}({instrumentationKey:"#{appInsightsKey}"});window.appInsights=appInsights;appInsights.trackPageView();
|
|
@ -0,0 +1,75 @@
|
|||
//-
|
||||
//- Copyright (c) Microsoft. All rights reserved.
|
||||
//- Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//-
|
||||
|
||||
extends layout
|
||||
|
||||
// Conditions for this page:
|
||||
// - not yet linked
|
||||
// - authenticated with AAD
|
||||
|
||||
block content
|
||||
div.container
|
||||
div.row
|
||||
div.col-md-7.col-lg-7
|
||||
h1 Link your accounts
|
||||
p.
|
||||
Let's make the lawyers happy by helping us associate your corporate and
|
||||
social coding accounts. Linking does not alter your GitHub account in any way.
|
||||
This allows us to answer the question <em>"who is #{user.github.displayUsernameTemporary || user.github.username}?"</em>
|
||||
and to give you the self-service tools to manage your open source work.
|
||||
table.table
|
||||
thead
|
||||
tr
|
||||
th GitHub User
|
||||
th #{config.companyName} Identity
|
||||
tbody
|
||||
tr
|
||||
td= user.github.displayUsernameTemporary || user.github.username
|
||||
td= user.azure.username
|
||||
p By continuing, you agree:
|
||||
ul
|
||||
li My GitHub account is controlled exclusively by #{user.azure.username}.
|
||||
li My GitHub password is safe, secure and smart.
|
||||
li I will enable two-factor authentication on the account and keep it active. I understand that I will lose access if I remove this security protection.
|
||||
form(method='post')
|
||||
p(style='margin-top:24px')
|
||||
input.btn.btn-primary(type='submit', value='I Agree')
|
||||
|
|
||||
a.btn.btn-default(href='/signout') Cancel
|
||||
hr
|
||||
h3 Your onboarding progress
|
||||
h5
|
||||
| Sign in with GitHub & #{config.companyName} IT
|
||||
|
|
||||
i.glyphicon.glyphicon-ok
|
||||
h5.text-primary
|
||||
| Link your identity
|
||||
h5
|
||||
| Join and accept your first organization invite from GitHub
|
||||
h5
|
||||
| Multifactor security checkup
|
||||
h5
|
||||
| Profile review
|
||||
h5
|
||||
| Publish your membership <em>(optional)</em>
|
||||
h5
|
||||
| Join a team <em>(optional)</em>
|
||||
|
||||
div.col-md-5.col-lg-5.alert-gray
|
||||
if user && user.github && user.github.id
|
||||
if config && config.corporate && config.corporate.trainingResources
|
||||
- var footres = config.corporate.trainingResources.footer
|
||||
if footres
|
||||
// These same resources appear on every single auth page footer, too.
|
||||
h3 Training & Resources
|
||||
p Bookmark these great resources today. These are important resources to grok.
|
||||
each categoryList, category in footres
|
||||
h5= category
|
||||
ul
|
||||
each item in categoryList
|
||||
li
|
||||
a(href=item.link, target='_new')
|
||||
= item.title + ' '
|
||||
i.glyphicon.glyphicon-share-alt
|
|
@ -0,0 +1,28 @@
|
|||
//-
|
||||
//- Copyright (c) Microsoft. All rights reserved.
|
||||
//- Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//-
|
||||
|
||||
extends layout
|
||||
|
||||
// Conditions for this page:
|
||||
// - linked
|
||||
// - NOT authenticated yet with AAD
|
||||
|
||||
block content
|
||||
div.container
|
||||
|
||||
h1 Please sign in with #{config.companyName} IT
|
||||
|
||||
p Hey #{user.github.displayName || user.github.username}:
|
||||
|
||||
p It looks like you have used a #{config.companyName} organization before, but this is the first time you are using the new open source portal.
|
||||
|
||||
p We need you to quickly authenticate with Azure Active Directory and your corporate identity this one time before you can access the portal. This should only take a minute or so depending on whether your corporate account has multi-factor (MFA) authentication already.
|
||||
|
||||
p This is also a good time to remind you of the policy and make sure the lawyers are happy.
|
||||
|
||||
p
|
||||
a.btn.btn-primary(href='/signin/azure') Sign In to #{config.companyName}
|
||||
|
|
||||
a.btn.btn-default(href='/signout') Cancel and Sign Out
|
|
@ -0,0 +1,32 @@
|
|||
//-
|
||||
//- Copyright (c) Microsoft. All rights reserved.
|
||||
//- Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//-
|
||||
|
||||
extends layout
|
||||
|
||||
block content
|
||||
div.container#top(style='margin-top:60px')
|
||||
div.container#content
|
||||
div.row
|
||||
div.col-md-10
|
||||
if messageTitle
|
||||
p
|
||||
em= messageTitle
|
||||
|
||||
if message
|
||||
h1= message
|
||||
if messageDetails
|
||||
p= messageDetails
|
||||
if messageTiny
|
||||
p
|
||||
small= messageTiny
|
||||
|
||||
p
|
||||
a.btn.btn-default(href='/') Home
|
||||
|
||||
if config && config.corporate && config.corporate.portalAdministratorEmail
|
||||
p
|
||||
| If you have any questions, please ping
|
||||
|
|
||||
a.alert-link(href='mailto:' + config.corporate.portalAdministratorEmail)= config.corporate.portalAdministratorEmail
|
|
@ -0,0 +1,112 @@
|
|||
//-
|
||||
//- Copyright (c) Microsoft. All rights reserved.
|
||||
//- Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//-
|
||||
|
||||
extends ../layout
|
||||
|
||||
block content
|
||||
div.container
|
||||
|
||||
if twoFactorOff === true
|
||||
|
||||
if notValidated
|
||||
h1 MFA is still not enabled for #{user.github.displayName || user.github.username}...
|
||||
else
|
||||
h1 2FA is not enabled
|
||||
|
||||
p.lead This GitHub org requires multi-factor authentication. Let's set it up now.
|
||||
|
||||
p If you already have an Authenticator app, this step takes <strong>2 minutes</strong>. If you need to install and configure an app for the first time, this will likely take <strong>5-10 minutes</strong>. This multi-factor setup is separate from your corporate authentication.
|
||||
|
||||
div.alert.alert-gray(role='alert')
|
||||
if notValidated
|
||||
strong Your GitHub account is still not protected with MFA
|
||||
else
|
||||
strong Two-factor auth is not turned on for your GitHub account
|
||||
p.
|
||||
Please enable 2FA on GitHub.com.
|
||||
|
||||
if notValidated
|
||||
p As of #{nowString} UTC, the GitHub API reports that your account is not as secure as it can be. By using a multi-factor app on your mobile device or signing up for SMS authentication messages, your account can be much more secure.
|
||||
|
||||
ul.list-inline
|
||||
li
|
||||
a.btn.btn-primary(href='https://github.com/settings/two_factor_authentication/configure', target='_new') Configure 2FA <i class="glyphicon glyphicon-share-alt"></i>
|
||||
li
|
||||
a.btn.btn-success(href=org.baseUrl + 'security-check?validate=validate' + (onboarding ? '&onboarding=' + onboarding : '')) Validate 2FA and Continue
|
||||
li
|
||||
a.btn.btn-default(href='/unlink') Cancel my corporate participation
|
||||
|
||||
p You can find out more about GitHub two-factor authentication online:
|
||||
|
||||
ul
|
||||
li <a href="https://github.com/blog/1614-two-factor-authentication">GitHub Blog Post about 2FA</a>
|
||||
li <a href="https://help.github.com/articles/about-two-factor-authentication/">GitHub 2FA Help</a>
|
||||
|
||||
hr
|
||||
|
||||
h2 Frequently Asked Questions
|
||||
|
||||
h3 I'm located overseas. What are my options for authenticating?
|
||||
|
||||
p GitHub supports some countries outside of the USA, but notably China is not included. If you are working in one of the countries that does not have SMS support from GitHub for MFA, you will need to use an Authenticator app that runs on your machine or smart phone. The app will provide the 2nd factor of authentication along with your password.
|
||||
|
||||
h3 What apps can I use for MFA?
|
||||
|
||||
p Most generic Authenticator apps should work. Make sure you trust the publisher. A starting point might be:
|
||||
|
||||
ul
|
||||
li
|
||||
p
|
||||
a(href='http://www.windowsphone.com/en-us/store/app/authenticator/e7994dbc-2336-4950-91ba-ca22d653759b', target='_self') Authenticator by Microsoft for Windows Phone
|
||||
br
|
||||
small Known issues with GitHub, QR scanning does not work
|
||||
li
|
||||
p
|
||||
a(href='https://itunes.apple.com/us/app/google-authenticator/id388497605', target='_self') Google Authenticator by Google for iPhone
|
||||
br
|
||||
small Works
|
||||
li
|
||||
p
|
||||
a(href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2', target='_self') Google Authenticator by Google for Android
|
||||
br
|
||||
small Likely works. Not verified.
|
||||
|
||||
h3 Will my password still work in apps and clients like Git Bash?
|
||||
|
||||
p Once 2FA is enabled, you will no longer use your password when pushing or pulling changes from GitHub. Instead, on GitHub you will generate a Personal Access Token to be used in the same manner as a password.
|
||||
|
||||
h3 What is Multi-Factor Authentication (MFA)? What about Two-Factor Authentication (2FA)?
|
||||
|
||||
p Same things. Read more on <a href="http://en.wikipedia.org/wiki/Multi-factor_authentication" target="_self">Wikipedia</a>.
|
||||
|
||||
h3 Can I use the Azure AD app, "Azure Authenticator", with GitHub?
|
||||
|
||||
p Unfortunately not. You will need a different app for this.
|
||||
|
||||
h3 I don't have a smart phone. What can I do?
|
||||
|
||||
p There are some apps out there that support MFA for Windows and other operating systems. You may want to look into that.
|
||||
|
||||
h3 I've lost my backup codes and other information. Can you help?
|
||||
|
||||
p The multi-factor relationship for your GitHub account (#{user.github.username}) is managed entirely by GitHub. When you first setup MFA, they will offer to let you download or print backup codes. You should consider doing this and storing them in a safe place. GitHub may not be able to ever grant you access back to your account if you lose the backup information and the MFA information.
|
||||
|
||||
h3 The Microsoft Authenticator app for Windows Phone is not working well with GitHub and the QR code.
|
||||
|
||||
p This is a known issue. You need to manually enter the code inside the Microsoft app for Windows Phone authentication, there are bugs in the app parsing the values that GitHub provides. <strong>:-(</strong>
|
||||
|
||||
h3 Other questions?
|
||||
|
||||
p Please use the <em>Contact Administrators</em> in the footer of this page with other questions or suggested Q&A entries. We'll try to get them up here.
|
||||
|
||||
else
|
||||
|
||||
h1 Two-factor security is enabled for #{user.github.displayName || user.github.username}
|
||||
|
||||
p.
|
||||
Thanks for helping to keep the organization secure.
|
||||
|
||||
p
|
||||
a.btn.btn-primary(href=org.baseUrl) Go to the #{org.name} portal
|
|
@ -0,0 +1,104 @@
|
|||
//-
|
||||
//- Copyright (c) Microsoft. All rights reserved.
|
||||
//- Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//-
|
||||
|
||||
extends ../layout
|
||||
|
||||
block content
|
||||
div.container
|
||||
if teamResponsibilities && teamResponsibilities.length && teamResponsibilities.length > 0
|
||||
h1 Approvals for Your Review
|
||||
div.container
|
||||
each entry in teamResponsibilities
|
||||
h2(style='padding-bottom: 16px')
|
||||
=entry.type === 'repo' ? 'New Repository' : 'Join a team'
|
||||
|
|
||||
small Permission Request
|
||||
div.row
|
||||
div.col-md-5.col-lg-5
|
||||
form(method='post', action='/' + entry._teamInstance.org.name + '/teams/' + entry._teamInstance.name + '/approvals/' + entry.RowKey)
|
||||
p
|
||||
a.btn.btn-sm.btn-muted(href='https://github.com/' + entry._teamInstance.org.name + '/' + entry._teamInstance.org.getWorkflowRepository().name + '/issues/' + entry.issue, target='_new')= entry.issue
|
||||
|
|
||||
a.btn.btn-sm.btn-muted(href='/' + entry._teamInstance.org.name + '/teams/' + entry._teamInstance.name + '/approvals/' + entry.RowKey)
|
||||
i.glyphicon.glyphicon-zoom-in
|
||||
|
|
||||
| View Detailed Request Page
|
||||
if entry.active === true
|
||||
p
|
||||
input.btn.btn-sm.btn-default(type='submit', name='approve', value='Approve')
|
||||
p
|
||||
input.btn.btn-sm.btn-primary(type='submit', name='approveWithComment', value='Approve with Comment...')
|
||||
p
|
||||
input.btn.btn-sm.btn-default(type='submit', name='deny', value='Deny...')
|
||||
div.col-md-7.col-lg-7
|
||||
p
|
||||
a.btn.btn-sm.btn-muted(href='https://github.com/' + entry.ghu, target='_new')= entry.ghu
|
||||
a.btn.btn-sm.btn-muted-more(href='mailto:' + entry.email)= entry.email ? entry.email : 'Unknown'
|
||||
if entry.type == 'repo' && entry.repoName
|
||||
h3 Repository Information
|
||||
blockquote
|
||||
p
|
||||
strong Name
|
||||
br
|
||||
= entry.repoName
|
||||
p
|
||||
strong Organization
|
||||
br
|
||||
= entry.org
|
||||
if entry.repoVisibility !== undefined
|
||||
p
|
||||
strong Visibility
|
||||
br
|
||||
= (entry.repoVisibility == 'public' ? 'Public' : 'Private')
|
||||
if entry.justification
|
||||
h3 Business Justification
|
||||
blockquote
|
||||
=entry.justification
|
||||
hr
|
||||
|
||||
h1 Requests you have made
|
||||
if usersRequests && usersRequests.length && usersRequests.length > 0
|
||||
div.container
|
||||
each myRequest in usersRequests
|
||||
div.row(style='margin-top:24px')
|
||||
div.col-md-5.col-lg-5
|
||||
if myRequest._teamInstance
|
||||
p
|
||||
a.btn.btn-sm.btn-muted(href='https://github.com/' + myRequest._teamInstance.org.name + '/' + myRequest._teamInstance.org.getWorkflowRepository().name + '/issues/' + myRequest.issue, target='_new') Tracking Issue ##{myRequest.issue}
|
||||
form(method='post', action='/approvals/' + myRequest.RowKey + '/cancel')
|
||||
p
|
||||
input.btn.btn-sm.btn-default(type='submit', value='Cancel my request')
|
||||
div.col-md-7.col-lg-7
|
||||
if myRequest.type == 'repo' && myRequest.repoName
|
||||
h3 Repository Information
|
||||
blockquote
|
||||
p
|
||||
strong Name
|
||||
br
|
||||
= myRequest.repoName
|
||||
p
|
||||
strong Organization
|
||||
br
|
||||
= myRequest.org
|
||||
if myRequest.repoVisibility !== undefined
|
||||
p
|
||||
strong Visibility
|
||||
br
|
||||
= (myRequest.repoVisibility == 'public' ? 'Public' : 'Private')
|
||||
else if myRequest.type == 'joinTeam'
|
||||
h3 Request to join a team
|
||||
h5 Organization
|
||||
p= myRequest.org
|
||||
h5 Team Name
|
||||
p= myRequest.teamname
|
||||
h5 GitHub Team ID
|
||||
p= myRequest.teamid
|
||||
if myRequest.justification
|
||||
h3 My Business Justification
|
||||
blockquote
|
||||
=myRequest.justification
|
||||
hr
|
||||
else
|
||||
p.lead There are no active requests open at this time.
|
|
@ -0,0 +1,254 @@
|
|||
//-
|
||||
//- Copyright (c) Microsoft. All rights reserved.
|
||||
//- Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//-
|
||||
|
||||
extends ../layout
|
||||
|
||||
block js_doc_ready
|
||||
| if (typeof initializeManageFilter == 'function') { initializeManageFilter(); }
|
||||
| if (typeof initializeMembershipFilter == 'function') { initializeMembershipFilter(); }
|
||||
|
||||
block content
|
||||
div.container
|
||||
|
||||
h1
|
||||
span.capitalize= org.name
|
||||
|
|
||||
small Organization
|
||||
|
||||
div.container
|
||||
div.row(style='margin-top:16px')
|
||||
div.col-md-3.col-lg-3
|
||||
div.metro-box(class=accountInfo.isMembershipPublic === true ? 'ms-green' : 'ms-yellow')
|
||||
a(href=org.baseUrl + 'membership')
|
||||
if accountInfo.isMembershipPublic === true
|
||||
h3 Public Member
|
||||
else
|
||||
h3 Concealed
|
||||
p= org.name.toUpperCase() + ' ORGANIZATION'
|
||||
div.col-md-3.col-lg-3
|
||||
div.metro-box.ms-blue
|
||||
a(href=org.baseUrl + 'teams')
|
||||
h3 Join a team
|
||||
p REQUEST ACCESS
|
||||
div.col-md-3.col-lg-3
|
||||
div.metro-box.ms-purple
|
||||
a(href='https://github.com/orgs/' + org.name + '/new-team', target='_new')
|
||||
h3 Create a team
|
||||
p NEW TEAM
|
||||
div.col-md-3.col-lg-3
|
||||
div.metro-box.ms-dark-blue
|
||||
a(href=org.baseUrl + 'new-repo')
|
||||
h3 Create a repo
|
||||
p REQUEST A NEW REPO
|
||||
div.row(style='margin-top:16px')
|
||||
div.col-md-3.col-lg-3
|
||||
if accountInfo.isMembershipPublic === true
|
||||
p Your membership is public and appears on your GitHub profile.
|
||||
else
|
||||
p Consider <a href="#{org.baseUrl}membership">taking your membership public</a> to help support our open source cause!
|
||||
div.col-md-3.col-lg-3
|
||||
p Request to join a team. Teams have permissions to a set of repos.
|
||||
div.col-md-3.col-lg-3
|
||||
p Create a team for new projects / assigning group permissions.
|
||||
div.col-md-3.col-lg-3
|
||||
p Request a new repo for your project.
|
||||
|
||||
if accountInfo.isSudoer
|
||||
h2 SUDO
|
||||
p Your account has sudoer rights for this organization. You have additional capabilities enabled to help ensure the health of the organization, its teams and repos. Please use care.
|
||||
|
||||
if accountInfo.teamsMaintained && accountInfo.teamsMaintained.length && accountInfo.teamsMaintained.length > 0
|
||||
if accountInfo.pendingApprovals && accountInfo.pendingApprovals.length && accountInfo.pendingApprovals.length > 0
|
||||
h2 Pending Approvals
|
||||
p
|
||||
a.btn.btn-default(href='./approvals/') See all pending #{org.name} approvals (#{accountInfo.pendingApprovals.length})
|
||||
if accountInfo.teamsMaintained
|
||||
h2.capitalize #{org.name} Teams You Maintain
|
||||
script(type='text/javascript').
|
||||
function initializeManageFilter() {
|
||||
var inputManageFilter = $('#manage-filter');
|
||||
if (inputManageFilter) {
|
||||
inputManageFilter.keyup(function () {;
|
||||
$.uiTableFilter($('table#manage-table'), this.value, ['Title', 'Organization', 'GitHub Name']);
|
||||
});
|
||||
}
|
||||
}
|
||||
div.container
|
||||
table.table#manage-table
|
||||
thead
|
||||
tr
|
||||
th(colspan='1')
|
||||
form#manage-filter-form
|
||||
input.form-control#manage-filter(name='filter', placeholder='Filter teams I manage', type='text')
|
||||
th
|
||||
p
|
||||
i.glyphicon.glyphicon-search
|
||||
tr
|
||||
th GitHub Name
|
||||
// th Organization
|
||||
th.thirtypercent Manage
|
||||
tbody
|
||||
each team in accountInfo.teamsMaintained
|
||||
tr
|
||||
td
|
||||
a.btn.btn-sm.btn-muted(href=org.baseUrl + 'teams/' + team.slug + '/')= team.name
|
||||
// td= team.org.name
|
||||
td.thirtypercent
|
||||
p
|
||||
a.btn.btn-sm.btn-default(href=org.baseUrl + 'teams/' + team.slug + '/') Manage Team
|
||||
|
||||
if accountInfo && accountInfo.membershipStatus === 'active' && accountInfo.isMembershipPublic !== true
|
||||
h1 Go public with your support of the #{org.name} org
|
||||
p Your profile on GitHub currently does not list your participation in the #{org.name} organization. By making your association public, others in the community will see you listed on the page for #{org.name} and your personal GitHub profile page will show the logo, too.
|
||||
p
|
||||
a.btn.btn-default.btn-sm(href=org.baseUrl + 'membership') Learn more
|
||||
|
||||
h1.capitalize #{org.name} Team Memberships
|
||||
|
||||
if accountInfo.teamsMaintainedHash
|
||||
p Here are teams that you are a member of but not a maintainer of.
|
||||
|
||||
if accountInfo.userTeamMemberships && accountInfo.userTeamMemberships.length && accountInfo.userTeamMemberships.length > 0
|
||||
script(type='text/javascript').
|
||||
function initializeMembershipFilter() {
|
||||
var inputMembershipFilter = $('#membership-filter');
|
||||
if (inputMembershipFilter) {
|
||||
inputMembershipFilter.keyup(function () {;
|
||||
$.uiTableFilter($('table#membership-table'), this.value, ['Team']);
|
||||
});
|
||||
}
|
||||
}
|
||||
table.table#membership-table
|
||||
thead
|
||||
tr
|
||||
th(colspan='1')
|
||||
form#membership-filter-form
|
||||
input.form-control#membership-filter(name='filter', placeholder='Filter my teams', type='text')
|
||||
th
|
||||
p
|
||||
i.glyphicon.glyphicon-search
|
||||
tr
|
||||
th Team
|
||||
th.thirtypercent View
|
||||
tbody
|
||||
- var everyoneTeamId = org.inner.settings.teamAllMembers
|
||||
each team in accountInfo.userTeamMemberships
|
||||
if team.id && accountInfo.teamsMaintainedHash && accountInfo.teamsMaintainedHash[team.id] !== undefined
|
||||
// Skipping this team since they are already maintaining it
|
||||
else
|
||||
tr
|
||||
td
|
||||
a.capitalize.btn.btn-sm.btn-muted(href='https://github.com/orgs/' + team.org.name + '/teams/' + team.slug, target='_new')= team.name
|
||||
td.thirtypercent
|
||||
p
|
||||
if everyoneTeamId == team.id
|
||||
a.btn.btn-default.btn-sm(href=team.org.baseUrl + 'leave') Leave Organization
|
||||
else
|
||||
a.btn.btn-default.btn-sm(href='https://github.com/orgs/' + team.org.name + '/teams/' + team.slug, target='_new')
|
||||
| Open on GitHub
|
||||
i.glyphicon.glyphicon-share-alt
|
||||
else
|
||||
//-p You are not currently a member of any GitHub teams that grant you permission to specific repositories. You may be pre-approved to join teams.
|
||||
p You are not currently a member of any GitHub teams for #{org.name}. <em>This view is cached.</em>
|
||||
p
|
||||
a.btn.btn-default(href=org.baseUrl + 'teams')= (accountInfo.userTeamMemberships && accountInfo.userTeamMemberships.length && accountInfo.userTeamMemberships.length > 0) ? 'Join another team' : 'Join a team'
|
||||
|
||||
if accountInfo.orgUser
|
||||
hr
|
||||
- var orgUser = accountInfo.orgUser
|
||||
h1(style='margin:36px 0') About the #{org.name} Organization
|
||||
div.row
|
||||
div.col-md-3.col-lg-3
|
||||
p
|
||||
img.img-thumbnail.img-responsive(src=orgUser.avatar(400), alt=(orgUser.name || orgUser.login))
|
||||
h3= orgUser.name
|
||||
h4= orgUser.login
|
||||
p(style='margin-top:18px')
|
||||
a.btn.btn-sm.btn-muted(href='https://github.com/' + org.name, target='_new')
|
||||
| View on GitHub
|
||||
i.glyphicon.glyphicon-share-alt
|
||||
div.col-md-8.col-lg-8.col-md-offset-1.col-lg-offset-1
|
||||
div.row
|
||||
div.col-md-6.col-lg-6
|
||||
if orgUser.company
|
||||
h6 Company
|
||||
p= orgUser.company
|
||||
if orgUser.location
|
||||
h6 Location
|
||||
p= orgUser.location
|
||||
if orgUser.email
|
||||
h6 E-mail
|
||||
p= orgUser.email
|
||||
if orgUser.otherFields.blog
|
||||
h6 On the Web
|
||||
p
|
||||
a(href=orgUser.otherFields.blog, target='_new')
|
||||
= orgUser.otherFields.blog
|
||||
|
|
||||
i.glyphicon.glyphicon-share-alt
|
||||
if orgUser.getProfileCreatedDate()
|
||||
h6 Created
|
||||
p
|
||||
time(datetime=orgUser.getProfileCreatedDate().toISOString())= orgUser.getProfileCreatedDate().toDateString()
|
||||
if orgUser.getProfileCreatedDate()
|
||||
h6 Updated
|
||||
p
|
||||
time(datetime=orgUser.getProfileUpdatedDate().toISOString())= orgUser.getProfileUpdatedDate().toDateString()
|
||||
hr
|
||||
if org.inner.settings.organizationPurpose
|
||||
h6 How we use this organization
|
||||
p= org.inner.settings.organizationPurpose
|
||||
if org.inner.settings.type
|
||||
h6 Supported Repository Types
|
||||
ul
|
||||
li Public Repositories
|
||||
if org.inner.settings.type == 'publicprivate'
|
||||
li Private Repositories
|
||||
|
||||
div.col-md-6.col-lg-h6
|
||||
h6 Repositories
|
||||
if orgUser.otherFields.public_repos
|
||||
h2
|
||||
= orgUser.otherFields.public_repos + ' '
|
||||
small Open Source
|
||||
//- small Public
|
||||
if orgUser.otherFields.total_private_repos
|
||||
h2
|
||||
= orgUser.otherFields.total_private_repos + ' '
|
||||
small Private
|
||||
|
||||
hr
|
||||
h6 #{config.companyName} Investment
|
||||
|
||||
if orgUser.otherFields.plan && orgUser.otherFields.plan.private_repos
|
||||
h2.capitalize
|
||||
= orgUser.otherFields.plan.name + ' '
|
||||
small Plan
|
||||
h2
|
||||
= (orgUser.otherFields.plan.private_repos - orgUser.otherFields.total_private_repos) + ' '
|
||||
small Remaining Private Repos
|
||||
h2
|
||||
| ∞
|
||||
small Remaining OSS Repos
|
||||
|
||||
if org.inner.settings.trainingResources
|
||||
- var tr = org.inner.settings.trainingResources
|
||||
if tr.organization && tr.organization.length && tr.organization.length > 0
|
||||
hr
|
||||
h3 Organization Resource#{tr.organization.length > 1 ? 's' : ''}
|
||||
ul.list-unstyled
|
||||
each resource in tr.organization
|
||||
li
|
||||
p
|
||||
a(href=resource.link, target='_new')
|
||||
= resource.title + ' '
|
||||
i.glyphicon.glyphicon-share-alt
|
||||
if resource.text
|
||||
br
|
||||
small= resource.text
|
||||
hr
|
||||
|
||||
p
|
||||
a.btn.btn-default(href=org.baseUrl + 'leave') Leave #{org.name}
|
|
@ -0,0 +1,29 @@
|
|||
//-
|
||||
//- Copyright (c) Microsoft. All rights reserved.
|
||||
//- Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//-
|
||||
|
||||
extends ../layout
|
||||
|
||||
// Conditions for this page:
|
||||
// - already an organization member
|
||||
|
||||
block content
|
||||
div.container
|
||||
|
||||
h1 We'll miss you!
|
||||
p.lead Are you sure that you want to leave the #{org.name} organization on GitHub?
|
||||
|
||||
p Please carefully review this page. Data loss is possible if you have forks of private organization repos.
|
||||
|
||||
p
|
||||
ul
|
||||
li Your GitHub account #{user.github.username} will be dropped from the org via the GitHub API immediately following this page
|
||||
li Any private forks of repos from #{org.name} will be deleted by GitHub
|
||||
li Any work done in a private fork of repos from #{org.name} will be lost. Data loss may happen.
|
||||
|
||||
form(method='post')
|
||||
p(style='margin-top:36px')
|
||||
input.btn.btn-primary(type='submit', value='Remove ' + user.github.username + ' from ' + org.name)
|
||||
|
|
||||
a.btn.btn-default(href=org.baseUrl) Cancel
|
|
@ -0,0 +1,182 @@
|
|||
//-
|
||||
//- Copyright (c) Microsoft. All rights reserved.
|
||||
//- Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//-
|
||||
|
||||
extends ../layout
|
||||
|
||||
block js_doc_ready
|
||||
| var inviteInterfaceUpdateOnClick = function(){$('#openInviteButton').removeClass('btn-primary').addClass('btn-muted');$('#inviteAcceptedButton').removeClass('btn-muted').addClass('btn-primary');alert('On the next page press the green Join button.\n\nThen close the page and return here to continue onboarding to #{org.name}.');return true;};
|
||||
| $('#openInviteButton').click(inviteInterfaceUpdateOnClick);
|
||||
| $('#openInviteButton2').click(inviteInterfaceUpdateOnClick);
|
||||
|
||||
block content
|
||||
div.container
|
||||
|
||||
if state == 'pending'
|
||||
|
||||
h1 You've been invited!
|
||||
|
||||
p.lead We've just had GitHub send you an invitation to <em>#{org.name}</em>.
|
||||
p
|
||||
| For security purposes, GitHub requires you to accept the invitation directly on their web site.
|
||||
strong You must come back to this tab after accepting your invitation to continue setting up your open source teams and permissions.
|
||||
div.row
|
||||
div.col-md-6.col-lg-6
|
||||
h3 Step 1: Accept your invite from GitHub
|
||||
p
|
||||
| Open your invite on GitHub and press the green button.
|
||||
p
|
||||
a.btn.btn-lg.capitalize.btn-primary#openInviteButton(href='https://github.com/orgs/' + org.name + '/invitation', target='_new') Open your #{org.name} invitation
|
||||
h3 Step 2: Return back here
|
||||
p
|
||||
| After pressing the green Join button, close the GitHub site and click Continue on this page. We'll then take you to a security check the "Join Teams" experience.
|
||||
p
|
||||
a.btn.btn-lg.btn-muted#inviteAcceptedButton(href=org.baseUrl + 'join' + (onboarding ? '?onboarding=' + onboarding : '')) I've accepted my invite on GitHub.com, continue...
|
||||
div.col-md-6.col-lg-6
|
||||
p(style='border:1px solid #ccc; padding:12px')
|
||||
a#openInviteButton2(target='_new', href='https://github.com/orgs/' + org.name + '/invitation')
|
||||
img.img-responsive(src='/img/GitHubInvitation.png', title='A screenshot of what the GitHub invitation looks like. The experience is hosted outside of this portal and actually on GitHub.com', alt='A screenshot of what the GitHub invitation looks like. The experience is hosted outside of this portal and actually on GitHub.com')
|
||||
br
|
||||
| A sample GitHub invite. Press the green button and close the page.
|
||||
|
||||
else if state == 'active'
|
||||
h1 You're now a member of #{org.name}.
|
||||
p You are currently a member of this additional organization. No additional work is required to gain access to it.
|
||||
p If you need to join a specific team to gain additional permissions, you can use the Join a Team experience on this site.
|
||||
p
|
||||
a.btn.btn-primary(href='/teams') Join a team
|
||||
|
||||
else
|
||||
div.row
|
||||
div.col-md-6.col-lg-6
|
||||
h1
|
||||
if onboarding
|
||||
| Let's join #{org.name}!
|
||||
else
|
||||
| Would you like to join #{org.name}?
|
||||
if user && user.github && user.github.increasedScope
|
||||
form(method='post')
|
||||
p(style='margin-top:24px')
|
||||
input.btn.btn-primary.btn-huge(type='submit', value='Join ' + org.name + ' now')
|
||||
else
|
||||
p.lead Quickest way: authorize this portal to join on my behalf
|
||||
p
|
||||
a.btn.btn-primary.btn-huge(href='/' + org.name + '/join/express' + (onboarding ? '?onboarding=' + onboarding : ''))
|
||||
| Join #{org.name} now
|
||||
p.
|
||||
Click to authorize this portal to use an additional GitHub API scope,
|
||||
<a href="https://developer.github.com/v3/oauth/#scopes" target="_new"><code>org:write</code></a>
|
||||
to automate the join process. <em>The additional scope will be removed the next time you sign
|
||||
in to this portal.</em>
|
||||
hr
|
||||
form(method='post')
|
||||
p
|
||||
input.btn.btn-primary.btn-lg(type='submit', value='Join ' + org.name + ' (Manual)')
|
||||
p.
|
||||
This alternate path does not increase the scope of permissions granted to this portal. After
|
||||
clicking the manual join button, we will 1) send you an invitation from #{org.name}, 2) ask
|
||||
you to accept the invitation over on GitHub.com, and then 3) verify that the invitation was
|
||||
accepted properly.
|
||||
ul
|
||||
li An invitation will be sent from GitHub
|
||||
li On the next page we'll explain how the GitHub invitation works
|
||||
li You will accept the invitation on the GitHub.com web site
|
||||
li We'll give you a chance to request to join teams in the organization
|
||||
if onboarding
|
||||
hr
|
||||
h3 Your onboarding progress
|
||||
h5
|
||||
| Sign in with GitHub & #{config.companyName} IT
|
||||
|
|
||||
i.glyphicon.glyphicon-ok
|
||||
h5
|
||||
| Link your identity
|
||||
|
|
||||
i.glyphicon.glyphicon-ok
|
||||
h5.text-primary
|
||||
| Join and accept your first organization invite from GitHub
|
||||
h5
|
||||
| Multifactor security checkup
|
||||
h5
|
||||
| Profile review
|
||||
h5
|
||||
| Publish your membership <em>(optional)</em>
|
||||
h5
|
||||
| Join a team <em>(optional)</em>
|
||||
|
||||
div.col-md-6.col-lg-6
|
||||
if orgUser
|
||||
p
|
||||
img.img-thumbnail.img-responsive(src=orgUser.avatar(400), alt=(orgUser.name || orgUser.login))
|
||||
h3= orgUser.name
|
||||
h4= orgUser.login
|
||||
p(style='margin-top:18px')
|
||||
a.btn.btn-sm.btn-muted(href='https://github.com/' + org.name, target='_new')
|
||||
| View on GitHub
|
||||
i.glyphicon.glyphicon-share-alt
|
||||
hr
|
||||
|
||||
div.row
|
||||
div.col-md-6.col-lg-6
|
||||
if orgUser.company
|
||||
h6 Company
|
||||
p= orgUser.company
|
||||
if orgUser.location
|
||||
h6 Location
|
||||
p= orgUser.location
|
||||
if orgUser.email
|
||||
h6 E-mail
|
||||
p= orgUser.email
|
||||
if orgUser.otherFields.blog
|
||||
h6 On the Web
|
||||
p
|
||||
a(href=orgUser.otherFields.blog, target='_new')
|
||||
= orgUser.otherFields.blog
|
||||
|
|
||||
i.glyphicon.glyphicon-share-alt
|
||||
if orgUser.getProfileCreatedDate()
|
||||
h6 Created
|
||||
p
|
||||
time(datetime=orgUser.getProfileCreatedDate().toISOString())= orgUser.getProfileCreatedDate().toDateString()
|
||||
if orgUser.getProfileCreatedDate()
|
||||
h6 Updated
|
||||
p
|
||||
time(datetime=orgUser.getProfileUpdatedDate().toISOString())= orgUser.getProfileUpdatedDate().toDateString()
|
||||
hr
|
||||
if org.inner.settings.organizationPurpose
|
||||
h6 How we use this organization
|
||||
p= org.inner.settings.organizationPurpose
|
||||
if org.inner.settings.type
|
||||
h6 Supported Repository Types
|
||||
ul
|
||||
li Public Repositories
|
||||
if org.inner.settings.type == 'publicprivate'
|
||||
li Private Repositories
|
||||
//-if org.inner.settings.type == 'publicprivate'
|
||||
p This organization has a billing relationship on behalf of #{config.companyName} to support private repos designated for open source use.
|
||||
|
||||
div.col-md-6.col-lg-h6
|
||||
h6 Repositories
|
||||
if orgUser.otherFields.public_repos
|
||||
h2
|
||||
= orgUser.otherFields.public_repos + ' '
|
||||
small Open Source
|
||||
//- small Public
|
||||
if orgUser.otherFields.total_private_repos
|
||||
h2
|
||||
= orgUser.otherFields.total_private_repos + ' '
|
||||
small Private
|
||||
|
||||
hr
|
||||
h6 #{config.companyName} Investment
|
||||
if orgUser.otherFields.plan && orgUser.otherFields.plan.private_repos
|
||||
h2.capitalize
|
||||
= orgUser.otherFields.plan.name + ' '
|
||||
small Plan
|
||||
h2
|
||||
= (orgUser.otherFields.plan.private_repos - orgUser.otherFields.total_private_repos) + ' '
|
||||
small Remaining Private Repos
|
||||
h2
|
||||
| ∞
|
||||
small Remaining OSS Repos
|