зеркало из
1
0
Форкнуть 0
This commit is contained in:
Jeff Wilcox 2015-11-13 12:38:55 -08:00
Коммит fd86c1b5c0
131 изменённых файлов: 20580 добавлений и 0 удалений

28
.gitignore поставляемый Normal file
Просмотреть файл

@ -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

6
.jshintignore Normal file
Просмотреть файл

@ -0,0 +1,6 @@
.git
node_modules
public/js/jquery*.*
public/js/html5*.*
public/js/bootstrap*.*
public/js/timeago*.*

25
LICENSE Normal file
Просмотреть файл

@ -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.

10
README.md Normal file
Просмотреть файл

@ -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)

51
app.js Normal file
Просмотреть файл

@ -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;

72
bin/www Normal file
Просмотреть файл

@ -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);
}
});

117
configuration.js Normal file
Просмотреть файл

@ -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;
};

639
data.js Normal file
Просмотреть файл

@ -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;

18
middleware/appInsights.js Normal file
Просмотреть файл

@ -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,
});
};

40
middleware/index.js Normal file
Просмотреть файл

@ -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);
};

17
middleware/locals.js Normal file
Просмотреть файл

@ -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();
};

27
middleware/logger.js Normal file
Просмотреть файл

@ -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();
};

25
middleware/scrubbedUrl.js Normal file
Просмотреть файл

@ -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();
};

26
middleware/session.js Normal file
Просмотреть файл

@ -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);
};

12
oss/audit.js Normal file
Просмотреть файл

@ -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();
}
};

581
oss/index.js Normal file
Просмотреть файл

@ -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;

36
oss/issue.js Normal file
Просмотреть файл

@ -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;

709
oss/org.js Normal file
Просмотреть файл

@ -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;

99
oss/redis.js Normal file
Просмотреть файл

@ -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;

314
oss/repo.js Normal file
Просмотреть файл

@ -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;

464
oss/team.js Normal file
Просмотреть файл

@ -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;

272
oss/user.js Normal file
Просмотреть файл

@ -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;

53
package.json Normal file
Просмотреть файл

@ -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"
}
}

5
public/browserconfig.xml Normal file
Просмотреть файл

@ -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.

6302
public/css/bootstrap.css поставляемый Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

7
public/css/bootstrap.min.css поставляемый Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

548
public/css/oss.css Normal file
Просмотреть файл

@ -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; }

Двоичные данные
public/favicon-144.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 534 B

Двоичные данные
public/favicon.ico Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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.

Двоичные данные
public/fonts/glyphicons-halflings-regular.eot Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -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="&#xd;" />
<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="&#xa0;" />
<glyph unicode="&#x2000;" horiz-adv-x="652" />
<glyph unicode="&#x2001;" horiz-adv-x="1304" />
<glyph unicode="&#x2002;" horiz-adv-x="652" />
<glyph unicode="&#x2003;" horiz-adv-x="1304" />
<glyph unicode="&#x2004;" horiz-adv-x="434" />
<glyph unicode="&#x2005;" horiz-adv-x="326" />
<glyph unicode="&#x2006;" horiz-adv-x="217" />
<glyph unicode="&#x2007;" horiz-adv-x="217" />
<glyph unicode="&#x2008;" horiz-adv-x="163" />
<glyph unicode="&#x2009;" horiz-adv-x="260" />
<glyph unicode="&#x200a;" horiz-adv-x="72" />
<glyph unicode="&#x202f;" horiz-adv-x="260" />
<glyph unicode="&#x205f;" horiz-adv-x="326" />
<glyph unicode="&#x20ac;" 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="&#x2212;" d="M200 400h900v300h-900v-300z" />
<glyph unicode="&#x25fc;" horiz-adv-x="500" d="M0 0z" />
<glyph unicode="&#x2601;" 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="&#x2709;" d="M0 100l400 400l200 -200l200 200l400 -400h-1200zM0 300v600l300 -300zM0 1100l600 -603l600 603h-1200zM900 600l300 300v-600z" />
<glyph unicode="&#x270f;" 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="&#xe001;" d="M0 1200h1200l-500 -550v-550h300v-100h-800v100h300v550z" />
<glyph unicode="&#xe002;" 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="&#xe003;" 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="&#xe005;" 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="&#xe006;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1z" />
<glyph unicode="&#xe007;" 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="&#xe008;" 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="&#xe009;" 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="&#xe010;" 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="&#xe011;" 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="&#xe012;" 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="&#xe013;" d="M29 454l419 -420l818 820l-212 212l-607 -607l-206 207z" />
<glyph unicode="&#xe014;" d="M106 318l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282l-212 -212l-282 282l-282 -282z" />
<glyph unicode="&#xe015;" 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="&#xe016;" 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="&#xe017;" 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="&#xe018;" d="M100 1h200v300h-200v-300zM400 1v500h200v-500h-200zM700 1v800h200v-800h-200zM1000 1v1200h200v-1200h-200z" />
<glyph unicode="&#xe019;" 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="&#xe020;" 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="&#xe021;" d="M1 601l656 644l644 -644h-200v-600h-300v400h-300v-400h-300v600h-200z" />
<glyph unicode="&#xe022;" 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="&#xe023;" 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="&#xe024;" d="M-100 0l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538l-41 400h-242l-40 -400h-539zM488 500h224l-27 300h-170z" />
<glyph unicode="&#xe025;" d="M0 0v400h490l-290 300h200v500h300v-500h200l-290 -300h490v-400h-1100zM813 200h175v100h-175v-100z" />
<glyph unicode="&#xe026;" 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="&#xe027;" 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="&#xe028;" 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="&#xe029;" 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="&#xe030;" 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="&#xe031;" 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="&#xe032;" 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="&#xe033;" 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="&#xe034;" 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="&#xe035;" 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="&#xe036;" 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="&#xe037;" 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="&#xe038;" 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="&#xe039;" 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="&#xe040;" 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="&#xe041;" 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="&#xe042;" 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="&#xe043;" d="M100 0v1025l175 175h925v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900z" />
<glyph unicode="&#xe044;" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" />
<glyph unicode="&#xe045;" 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="&#xe046;" 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="&#xe047;" 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="&#xe048;" 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="&#xe049;" 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="&#xe050;" 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="&#xe051;" 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="&#xe052;" 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="&#xe053;" 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="&#xe054;" 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="&#xe055;" 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="&#xe056;" 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="&#xe057;" 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="&#xe058;" 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="&#xe059;" 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="&#xe060;" 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="&#xe062;" 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="&#xe063;" 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="&#xe064;" 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="&#xe065;" 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="&#xe066;" 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="&#xe067;" 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="&#xe068;" d="M0 603l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296l-300 -300v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198z" />
<glyph unicode="&#xe069;" 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="&#xe070;" 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="&#xe071;" d="M136 550l564 550v-487l500 487v-1100l-500 488v-488z" />
<glyph unicode="&#xe072;" d="M200 0l900 550l-900 550v-1100z" />
<glyph unicode="&#xe073;" 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="&#xe074;" 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="&#xe075;" d="M0 0v1100l500 -487v487l564 -550l-564 -550v488z" />
<glyph unicode="&#xe076;" 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="&#xe077;" 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="&#xe078;" 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="&#xe079;" d="M185 599l592 -592l240 240l-353 353l353 353l-240 240z" />
<glyph unicode="&#xe080;" d="M272 194l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1l-592 -591z" />
<glyph unicode="&#xe081;" 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="&#xe082;" 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="&#xe083;" 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="&#xe084;" 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="&#xe085;" 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="&#xe086;" 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="&#xe087;" 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="&#xe088;" 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="&#xe089;" 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="&#xe090;" 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="&#xe091;" d="M0 547l600 453v-300h600v-300h-600v-301z" />
<glyph unicode="&#xe092;" d="M0 400v300h600v300l600 -453l-600 -448v301h-600z" />
<glyph unicode="&#xe093;" d="M204 600l450 600l444 -600h-298v-600h-300v600h-296z" />
<glyph unicode="&#xe094;" d="M104 600h296v600h300v-600h298l-449 -600z" />
<glyph unicode="&#xe095;" 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="&#xe096;" d="M0 0v400l129 -129l294 294l142 -142l-294 -294l129 -129h-400zM635 777l142 -142l294 294l129 -129v400h-400l129 -129z" />
<glyph unicode="&#xe097;" d="M34 176l295 295l-129 129h400v-400l-129 130l-295 -295zM600 600v400l129 -129l295 295l142 -141l-295 -295l129 -130h-400z" />
<glyph unicode="&#xe101;" 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="&#xe102;" 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="&#xe103;" 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="&#xe104;" 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="&#xe105;" 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="&#xe106;" 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="&#xe107;" 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="&#xe108;" 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="&#xe109;" 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="&#xe110;" 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="&#xe111;" 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="&#xe112;" 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="&#xe113;" d="M-30 411l227 -227l352 353l353 -353l226 227l-578 579z" />
<glyph unicode="&#xe114;" d="M70 797l580 -579l578 579l-226 227l-353 -353l-352 353z" />
<glyph unicode="&#xe115;" d="M-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196zM402 1000l215 -200h381v-400h-198l299 -283l299 283h-200v600h-796z" />
<glyph unicode="&#xe116;" 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="&#xe117;" 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="&#xe118;" 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="&#xe119;" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" />
<glyph unicode="&#xe120;" d="M0 600l300 298v-198h600v198l300 -298l-300 -297v197h-600v-197z" />
<glyph unicode="&#xe121;" 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="&#xe122;" 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="&#xe123;" 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="&#xe124;" 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="&#xe125;" 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="&#xe126;" 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="&#xe127;" 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="&#xe128;" 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="&#xe129;" 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="&#xe130;" 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="&#xe131;" 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="&#xe132;" 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="&#xe133;" 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="&#xe134;" 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="&#xe135;" 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="&#xe136;" 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="&#xe137;" 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="&#xe138;" d="M100 1100v100h1000v-100h-1000zM150 1000h900l-350 -500v-300l-200 -200v500z" />
<glyph unicode="&#xe139;" 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="&#xe140;" 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="&#xe141;" 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="&#xe142;" 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="&#xe143;" 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="&#xe144;" 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="&#xe145;" 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="&#xe146;" 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="&#xe148;" 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="&#xe149;" 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="&#xe150;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM602 900l298 300l298 -300h-198v-900h-200v900h-198z" />
<glyph unicode="&#xe151;" 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="&#xe152;" 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="&#xe153;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 100v400h300v-500h-100v100h-200zM800 1100v100h200v-500h-100v400h-100zM901 200h100v200h-100v-200z" />
<glyph unicode="&#xe154;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 400v100h200v-500h-100v400h-100zM800 800v400h300v-500h-100v100h-200zM901 900h100v200h-100v-200z" />
<glyph unicode="&#xe155;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h500v-200h-500zM700 400v200h400v-200h-400zM700 700v200h300v-200h-300zM700 1000v200h200v-200h-200z" />
<glyph unicode="&#xe156;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h200v-200h-200zM700 400v200h300v-200h-300zM700 700v200h400v-200h-400zM700 1000v200h500v-200h-500z" />
<glyph unicode="&#xe157;" 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="&#xe158;" 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="&#xe159;" 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="&#xe160;" 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="&#xe161;" 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="&#xe162;" 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="&#xe163;" 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="&#xe164;" 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="&#xe165;" 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="&#xe166;" 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="&#xe167;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 700h300v-300h300v300h295l-445 500zM900 150h100v50h-100v-50z" />
<glyph unicode="&#xe168;" 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="&#xe169;" 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="&#xe170;" 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="&#xe171;" d="M23 415l1177 784v-1079l-475 272l-310 -393v416h-392zM494 210l672 938l-672 -712v-226z" />
<glyph unicode="&#xe172;" 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="&#xe173;" 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="&#xe174;" 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="&#xe175;" 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="&#xe176;" 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="&#xe177;" 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="&#xe178;" 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="&#xe179;" 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="&#xe180;" 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="&#xe181;" 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="&#xe182;" 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="&#xe183;" 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="&#xe184;" d="M100 0v100h1100v-100h-1100zM175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250z" />
<glyph unicode="&#xe185;" 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="&#xe186;" 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="&#xe187;" 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="&#xe188;" 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="&#xe189;" 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="&#xe190;" 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="&#xe191;" 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="&#xe192;" 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="&#xe193;" 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="&#xe194;" 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="&#xe195;" 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="&#xe197;" 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="&#xe198;" 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="&#xe199;" d="M100 200h400v-155l-75 -45h350l-75 45v155h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170z" />
<glyph unicode="&#xe200;" 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

Двоичные данные
public/fonts/glyphicons-halflings-regular.ttf Normal file

Двоичный файл не отображается.

Двоичные данные
public/fonts/glyphicons-halflings-regular.woff Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -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.

Двоичные данные
public/img/GitHubInvitation.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 37 KiB

Двоичные данные
public/img/glyphicons-halflings-white.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 8.6 KiB

Двоичные данные
public/img/glyphicons-halflings.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 12 KiB

Двоичные данные
public/img/rainycloud.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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.

2114
public/js/bootstrap.js поставляемый Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

6
public/js/bootstrap.min.js поставляемый Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

8
public/js/html5shiv.js поставляемый Normal file
Просмотреть файл

@ -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);

4
public/js/jquery.min.js поставляемый Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -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);

214
public/js/timeago.js Normal file
Просмотреть файл

@ -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");
}));

2
public/robots.txt Normal file
Просмотреть файл

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

70
resources.json Normal file
Просмотреть файл

@ -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."
}
]
}
}

212
routes/approvals.js Normal file
Просмотреть файл

@ -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;

84
routes/index-linked.js Normal file
Просмотреть файл

@ -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;

27
routes/index.js Normal file
Просмотреть файл

@ -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;

45
routes/legacy.js Normal file
Просмотреть файл

@ -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;

64
routes/link.js Normal file
Просмотреть файл

@ -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;

42
routes/org/2fa.js Normal file
Просмотреть файл

@ -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;

110
routes/org/index.js Normal file
Просмотреть файл

@ -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;

92
routes/org/join.js Normal file
Просмотреть файл

@ -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;

78
routes/org/leave.js Normal file
Просмотреть файл

@ -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;

56
routes/org/membership.js Normal file
Просмотреть файл

@ -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;

190
routes/org/requestRepo.js Normal file
Просмотреть файл

@ -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;

159
routes/org/team/index.js Normal file
Просмотреть файл

@ -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;

177
routes/org/team/members.js Normal file
Просмотреть файл

@ -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;

49
routes/org/team/repos.js Normal file
Просмотреть файл

@ -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;

100
routes/org/teams.js Normal file
Просмотреть файл

@ -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;

243
routes/orgAdmin.js Normal file
Просмотреть файл

@ -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;

30
routes/orgs.js Normal file
Просмотреть файл

@ -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;

41
routes/thanks.js Normal file
Просмотреть файл

@ -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;

68
routes/unlink.js Normal file
Просмотреть файл

@ -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;

185
utils.js Normal file
Просмотреть файл

@ -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;
};

45
views/error.jade Normal file
Просмотреть файл

@ -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}

33
views/home.jade Normal file
Просмотреть файл

@ -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

100
views/index.jade Normal file
Просмотреть файл

@ -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

175
views/layout.jade Normal file
Просмотреть файл

@ -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') &times;
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') &times;
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
| &middot;
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
| &copy; #{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();

75
views/link.jade Normal file
Просмотреть файл

@ -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')
| &nbsp; &nbsp;
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

28
views/linkUpdate.jade Normal file
Просмотреть файл

@ -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}
| &nbsp;
a.btn.btn-default(href='/signout') Cancel and Sign Out

32
views/message.jade Normal file
Просмотреть файл

@ -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

112
views/org/2fa.jade Normal file
Просмотреть файл

@ -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

104
views/org/approvals.jade Normal file
Просмотреть файл

@ -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
| &nbsp;
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.

254
views/org/index.jade Normal file
Просмотреть файл

@ -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
| &infin;&nbsp;
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}

29
views/org/leave.jade Normal file
Просмотреть файл

@ -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)
| &nbsp;&nbsp;&nbsp;
a.btn.btn-default(href=org.baseUrl) Cancel

182
views/org/pending.jade Normal file
Просмотреть файл

@ -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
| &infin;&nbsp;
small Remaining OSS Repos

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше