зеркало из
1
0
Форкнуть 0
This commit is contained in:
Jeff Wilcox 2016-07-19 16:12:23 -07:00
Родитель 82b6dd84e1
Коммит 7478b17678
14 изменённых файлов: 269 добавлений и 116 удалений

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

@ -62,6 +62,6 @@ module.exports = function (err, req, res, next) {
error: {},
title: err.status === 404 ? 'Not Found' : 'Oops',
user: req.user,
config: config,
config: config.obfuscatedConfig,
});
};

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

@ -38,6 +38,7 @@ module.exports = function configurePassport(app, passport, initialConfig) {
app.get('/signin/github/join', (req, res) => {
res.render('creategithubaccount', {
title: 'Create a GitHub account',
user: req.user,
});
});
@ -80,6 +81,9 @@ module.exports = function configurePassport(app, passport, initialConfig) {
delete req.user.github;
}
var url = req.headers.referer || '/';
if (req.query.redirect === 'github') {
url = 'https://github.com/logout';
}
res.redirect(url);
});

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

@ -13,8 +13,27 @@ const Team = require('./team');
const User = require('./user');
const RedisHelper = require('./redis');
function OpenSourceUserContext(applicationConfiguration, dataClient, user, redisInstance, callback) {
function OpenSourceUserContext(options, callback) {
var self = this;
self.displayNames = {
github: null,
};
self.usernames = {
github: null,
azure: null,
};
self.id = {
github: null,
};
self.entities = {
link: null,
primaryMembership: null,
};
var applicationConfiguration = options.config;
var dataClient = options.dataClient;
var redisInstance = options.redisClient;
var requestUser = options.request ? options.request.user : null;
var link = options.link;
var modernUser;
this.cache = {
orgs: {},
@ -26,6 +45,7 @@ function OpenSourceUserContext(applicationConfiguration, dataClient, user, redis
this.createModernUser = function (id, login) {
modernUser = new User(this, id);
modernUser.login = login;
return modernUser;
};
this.setting = function (name) {
return applicationConfiguration[name];
@ -36,89 +56,117 @@ function OpenSourceUserContext(applicationConfiguration, dataClient, user, redis
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) {
this.createModernUser(this.id.github, this.usernames.github);
}
this.safeConfiguration = safeSettings(applicationConfiguration);
this.baseUrl = '/';
this.redis = new RedisHelper(this, applicationConfiguration.redis.prefix);
this.initializeBasics(function (initError) {
if (callback) {
return callback(initError, self);
}
});
if (link && options.request) {
return callback(new Error('The context cannot be set from both a request and a link instance.'));
}
if (link) {
return setPropertiesFromLink(self, link, callback);
}
if (options.request) {
return this.resolveLinkFromRequest(options.request, callback);
}
callback(new Error('Could not initialize the context for the acting user.'), self);
}
function setPropertiesFromLink(context, link, callback) {
context.usernames.github = link.ghu;
context.id.github = link.ghid.toString();
context.usernames.azure = link.aadupn;
context.entities.link = link;
var modernUser = context.modernUser();
if (!modernUser && context.id.github) {
modernUser = context.createModernUser(context.id.github, context.usernames.github);
}
modernUser.link = link;
callback(null, context);
}
function tooManyLinksError(self, userLinks, callback) {
var tooManyLinksError = new Error(`This account has ${userLinks.length} linked GitHub accounts.`);
tooManyLinksError.links = userLinks;
tooManyLinksError.tooManyLinks = true;
return callback(tooManyLinksError, self);
}
function existingGitHubIdentityError(self, link, requestUser, callback) {
var endUser = requestUser.azure.displayName || requestUser.azure.username;
var obfuscatedUsername = utils.obfuscate(link.ghu, 4);
var anotherGitHubAccountError = new Error(`${endUser}, there is a different GitHub account linked to your corporate identity.`);
anotherGitHubAccountError.anotherAccount = true;
anotherGitHubAccountError.detailed = `If you need to switch which account is associated with your identity, please unlink the old account first. Your other GitHub account username ends in: ${obfuscatedUsername}.`;
anotherGitHubAccountError.fancyLink = {
link: '/signout/github/?redirect=github',
title: `Sign Out ${requestUser.github.username} on GitHub`,
};
return callback(anotherGitHubAccountError, self);
}
// ----------------------------------------------------------------------------
// Populate the user's OSS context object.
// ----------------------------------------------------------------------------
OpenSourceUserContext.prototype.initializeBasics = function (callback) {
OpenSourceUserContext.prototype.resolveLinkFromRequest = function (request, callback) {
var self = this;
var requestUser = this.requestUser();
if (!userObject && this.setting('primaryAuthenticationScheme') === 'aad' && requestUser.azure && requestUser.azure.username) {
return this.dataClient().getUserByAadUpn(requestUser.azure.username, function (findError, userLinks) {
var requestUser = request.user;
if (requestUser && requestUser.github) {
self.usernames.github = requestUser.github.username;
self.id.github = requestUser.github.id;
self.displayNames.github = requestUser.github.displayName;
}
if (requestUser && requestUser.azure) {
self.usernames.azure = requestUser.azure.username;
}
if (self.setting('primaryAuthenticationScheme') === 'aad' && requestUser.azure && requestUser.azure.username) {
return self.dataClient().getUserByAadUpn(requestUser.azure.username, function (findError, userLinks) {
if (findError) {
// XXX: wrap with a useful message?
return callback(findError);
return callback(utils.wrapError(findError, 'There was a problem trying to load the link for the active user.'), self);
}
if (userLinks.length === 0) {
return callback();
return callback(null, self);
}
if (userLinks.length > 1) {
var tooManyLinksError = new Error(`This account has ${userLinks.length} linked GitHub accounts.`);
tooManyLinksError.links = userLinks;
tooManyLinksError.tooManyLinks = true;
return callback(tooManyLinksError);
return tooManyLinksError(self, userLinks, callback);
}
var link = userLinks[0];
self.usernames.github = link.ghu;
self.id.github = link.ghid.toString();
self.createModernUser(self.id.github, self.usernames.github);
self.entities.link = link;
self.modernUser().link = link;
// todo: if their AAD name or upn has changed, but oid is still the same... schedule an update!
// question: should this.authenticated.github be true or false, since it isn't authenticated yet?
callback(null, false);
if (requestUser.github && requestUser.github.username && link.ghu !== requestUser.github.username) {
if (link.ghid === requestUser.github.id) {
// TODO: The user has changed their GitHub name, simply update the link record.
} else {
return existingGitHubIdentityError(self, link, requestUser, callback);
}
}
if (requestUser.azure.displayName !== link.aadname) {
// TODO: update their display name?
console.warn(`The user's Azure display name has changed. Link: ${link.aadname}, Passport: ${requestUser.azure.displayName}`);
}
if (requestUser.azure.username !== link.aadupn) {
// Confirm that their OID is the same
// RARE: TODO: ??? update their AAD upn in the link
// if OID has changed, freak out?
console.warn(`The user's Azure username has changed. Link: ${link.aadupn}, Passport: ${requestUser.azure.username}`);
}
return setPropertiesFromLink(self, link, callback);
});
}
var userObject = this.modernUser();
var userObject;
if (self.id.github) {
userObject = self.createModernUser(self.id.github, self.usernames.github);
}
if (!userObject) {
return callback(new Error('There\'s a logic bug in the user context object. We cannot continue.'));
return callback(new Error('There\'s a logic bug in the user context object. We cannot continue.'), self);
}
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.'));
return callback(utils.wrapError(error, 'We were not able to retrieve information about any link for your user account at this time.'), self);
}
if (link) {
self.entities.link = link;
// CONSIDER: If the link values differ from the request properties...
return setPropertiesFromLink(self, link, callback);
} else {
callback(null, self);
}
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);
});
*/
});
};
@ -559,8 +607,7 @@ OpenSourceUserContext.prototype.saveUserAlert = function (req, message, title, c
};
function safeSettings(config) {
// CONSIDER: IMPLEMENT.
return config;
return config.obfuscatedConfig;
}
// ----------------------------------------------------------------------------
@ -578,11 +625,31 @@ OpenSourceUserContext.prototype.render = function (req, res, view, title, option
if (breadcrumbs && breadcrumbs.length && breadcrumbs.length > 0) {
breadcrumbs[breadcrumbs.length - 1].isLast = true;
}
var authScheme = this.setting('primaryAuthenticationScheme');
var user = {
primaryAuthenticationScheme: authScheme,
primaryUsername: authScheme === 'github' ? this.usernames.github : this.usernames.azure,
githubSignout: authScheme === 'github' ? '/signout' : '/signout/github',
azureSignout: authScheme === 'github' ? '/signout/azure' : '/signout',
};
if (this.id.github || this.usernames.github) {
user.github = {
id: this.id.github,
username: this.usernames.github,
displayName: this.displayNames.github,
increasedScope: null, // TODO: Increased scope should be set in the pipeline
};
}
if (this.usernames.azure) {
user.azure = {
username: this.usernames.azure,
};
}
var obj = {
title: title,
config: this.safeConfigurationTemp,
config: this.safeConfiguration,
serviceBanner: this.setting('serviceBanner'),
user: this.requestUser(),
user: user,
ossLink: this.entities.link,
showBreadcrumbs: true,
breadcrumbs: breadcrumbs,

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

@ -3,6 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
const async = require('async');
const utils = require('../utils');
const debug = require('debug')('azureossportal');
@ -165,12 +166,12 @@ OpenSourceUser.prototype.unlinkAndDrop = function (callback) {
if (getOrganizationsError) {
return callback(getOrganizationsError);
}
async.each(currentOrganizationMemberships, function (org, callback) {
async.each(currentOrganizationMemberships, function (org, cb) {
org.removeUserMembership(function () {
// TODO: 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();
cb();
});
}, function (/* ignored error per above statement */) {
var dc = self.oss.dataClient();

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

@ -210,6 +210,22 @@ footer {
margin-bottom: 16px;
}
.pad, vertical-pad, .vertical-pad-top {
padding-top: 16px;
}
.pad, vertical-pad, .vertical-pad-bottom {
padding-bottom: 16px;
}
.pad, .horizontal-pad, .horizontal-pad-left {
padding-left: 16px;
}
.pad, .horizontal-pad, .horizontal-pad-right {
padding-right: 16px;
}
code {
color: #333;
}

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

@ -14,17 +14,17 @@ const linkCleanupRoute = require('./link-cleanup');
router.use(function (req, res, next) {
var config = req.app.settings.runtimeConfig;
if (req.isAuthenticated()) {
if (req.user && !req.user.github) {
// no github info
// IF secondary auth,... 1) check for a link
// return next(new Error('We do not have your GitHub stuff.'));
var expectedAuthenticationProperty = config.primaryAuthenticationScheme === 'github' ? 'github' : 'azure';
if (req.user && !req.user[expectedAuthenticationProperty]) {
console.warn(`A user session was authenticated but did not have present the property ${expectedAuthenticationProperty} expected for this type of authentication. Signing them out.`);
return res.redirect('/signout');
}
if (req.user && req.user.github && !req.user.github.id) {
return next(new Error('Invalid GitHub user information provided by GitHub.'));
var expectedAuthenticationKey = config.primaryAuthenticationScheme === 'github' ? 'id' : 'username';
if (!req.user[expectedAuthenticationProperty][expectedAuthenticationKey]) {
return next(new Error('Invalid information present for the authentication provider.'));
}
return next();
}
var url = req.originalUrl;
if (url) {
if (req.session) {
@ -36,9 +36,13 @@ router.use(function (req, res, next) {
});
router.use((req, res, next) => {
var config = req.app.settings.runtimeConfig;
var dc = req.app.settings.dataclient;
new OpenSourceUserContext(config, dc, req.user, dc.cleanupInTheFuture.redisClient, function (error, instance) {
var options = {
config: req.app.settings.runtimeConfig,
dataClient: req.app.settings.dataclient,
redisClient: req.app.settings.dataclient.cleanupInTheFuture.redisClient,
request: req,
};
new OpenSourceUserContext(options, (error, instance) => {
req.oss = instance;
if (error && error.tooManyLinks === true) {
if (req.url === '/link-cleanup') {
@ -160,7 +164,7 @@ router.get('/', function (req, res, next) {
onboarding = true;
}
var render = function (results) {
var pageTitle = results && results.userOrgMembership === false ? 'My GitHub Account' : config.companyName + ' - Open Source Portal for GitHub';
var pageTitle = results && results.userOrgMembership === false ? 'My GitHub Account' : config.companyName + ' - ' + config.portalName;
oss.render(req, res, 'index', pageTitle, {
accountInfo: results,
onboarding: onboarding,

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

@ -10,6 +10,7 @@ const router = express.Router();
const async = require('async');
const moment = require('moment');
const utils = require('../utils');
const OpenSourceUserContext = require('../oss');
// Enforcing just a single GitHub account per Active Directory user. With
// mild refactoring, this portal could easily support a session selecting
@ -37,19 +38,20 @@ router.use((req, res, next) => {
});
});
function renderCleanupPage(req, res, optionalUsernameToConfirm) {
function renderCleanupPage(req, res, idToConfirm, links) {
links = links || req.linksForCleanup;
let twoColumns = [[], []];
for (let i = 0; i < req.linksForCleanup.length; i++) {
if (req.linksForCleanup[i].joined) {
req.linksForCleanup[i].joinedDate = new Date(Math.round(req.linksForCleanup[i].joined));
for (let i = 0; i < links.length; i++) {
if (links[i].joined) {
links[i].joinedDate = new Date(Math.round(links[i].joined));
}
twoColumns[i % 2].push(req.linksForCleanup[i]);
twoColumns[i % 2].push(links[i]);
}
req.oss.render(req, res, 'multiplegithubaccounts', 'GitHub Cleanup', {
linksForCleanupByColumn: twoColumns,
numberToRemove: req.linksForCleanup.length - 1,
confirming: optionalUsernameToConfirm,
})
confirming: idToConfirm,
});
}
router.get('/', (req, res, next) => {
@ -57,20 +59,45 @@ router.get('/', (req, res, next) => {
});
router.post('/', (req, res, next) => {
let username = req.body.unlink;
let isConfirming = req.body.confirm === username;
if (!isConfirming) {
return renderCleanupPage(req, res, username);
let id = req.body.unlink;
let link = null;
let remainingLinks = [];
for (let i = 0; i < req.linksForCleanup.length; i++) {
if (req.linksForCleanup[i].ghid === id) {
link = req.linksForCleanup[i];
} else {
remainingLinks.push(req.linksForCleanup[i]);
}
}
if (!link) {
return next(new Error(`Could not identify the link for GitHub user ${id}.`));
}
let isConfirming = req.body.confirm === id;
if (!isConfirming) {
return renderCleanupPage(req, res, id);
}
var options = {
config: req.app.settings.runtimeConfig,
dataClient: req.app.settings.dataclient,
redisClient: req.app.settings.dataclient.cleanupInTheFuture.redisClient,
link: link,
};
new OpenSourceUserContext(options, function (contextError, unlinkContext) {
if (contextError) {
return next(contextError);
}
unlinkContext.modernUser().unlinkAndDrop((unlinkError) => {
if (unlinkError) {
return next(unlinkError);
}
if (remainingLinks.length > 1) {
renderCleanupPage(req, res, null, remainingLinks);
} else {
req.oss.saveUserAlert(req, link.ghu + ' has been unlinked. You now have just one GitHub account link.', 'Link cleanup complete', 'success');
res.redirect('/');
}
});
});
});
/* var link = userLinks[0];
self.usernames.github = link.ghu;
self.id.github = link.ghid.toString();
self.createModernUser(self.id.github, self.usernames.github);
self.entities.link = link;
self.modernUser().link = link;
*/
module.exports = router;

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

@ -45,7 +45,11 @@ router.post('/', function (req, res, next) {
});
router.get('/update', function (req, res, next) {
var config = req.app.settings.runtimeConfig;
var oss = req.oss;
if (config.primaryAuthenticationScheme === 'aad'){
return next('Changing a GitHub account is not yet supported.');
}
if (!(oss.usernames.azure)) {
return oss.render(req, res, 'linkUpdate', 'Update your account ' + oss.usernames.github + ' by signing in with corporate credentials.');
}

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

@ -28,14 +28,13 @@ function getPackageInfo() {
}
router.get('/', function (req, res) {
var config = req.app.settings.runtimeConfig;
var config = req.app.settings.runtimeConfig.obfuscatedConfig;
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
title: 'Open Source Components',
});
});

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

@ -160,6 +160,26 @@ exports.arrayToHashById = function athi(inputArray) {
return hash;
};
// ----------------------------------------------------------------------------
// Obfuscate a string value, optionally leaving a few characters visible.
// ----------------------------------------------------------------------------
exports.obfuscate = function obfuscate(value, lastCharactersShowCount) {
if (value === undefined || value === null || value.length === undefined) {
return value;
}
var length = value.length;
lastCharactersShowCount = lastCharactersShowCount || 0;
lastCharactersShowCount = Math.min(lastCharactersShowCount, length - 1);
var obfuscated = '';
for (var i = 0; i < length - lastCharactersShowCount; i++) {
obfuscated += '*';
}
for (var j = length - lastCharactersShowCount; j < length; j++) {
obfuscated += value[j];
}
return obfuscated;
}
// ----------------------------------------------------------------------------
// A very basic breadcrumb stack that ties in to an Express request object.
// ----------------------------------------------------------------------------

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

@ -7,12 +7,12 @@ doctype html
html(lang="en")
head
meta(charset='utf-8')
title= (user && user.github && user.github.username) ? title + ' - ' + user.github.username : title
title= (user && user.primaryUsername) ? title + ' - ' + user.primaryUsername : 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='Microsoft Open Source')
link(href='/css/bootstrap.min.css?1', rel='stylesheet')
link(href='/repos-css/oss.css?2a', rel='stylesheet')
link(href='/repos-css/oss.css?2b', 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')
@ -73,6 +73,8 @@ html(lang="en")
nav(role='navigation')
div.container(style='margin-top:24px;margin-bottom:12px')
div.row(style=(user && !error && ossLink) ? 'margin-left:0' : 'margin-left:-30px')
- var githubSignout = user && user.githubSignout ? user.githubSignout : '/signout'
- var azureSignout = user && user.azureSignout ? user.azureSignout : '/signout'
div.col-md-6
if user && user.github
h4 Your GitHub Account
@ -84,10 +86,16 @@ html(lang="en")
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
if primaryAuthenticationScheme == 'github'
a.btn.btn-sm.btn-white(href=githubSignout, style='margin-left:10px') Sign Out
else
a.btn.btn-sm.btn-white(href='/link/update', style='margin-left:10px') Change
else
p
small Sign in or create your GitHub.com account to manage your #{config && config.companyName || 'corporate'} open source identity.
if primaryAuthenticationScheme == 'github'
small Sign in or create your GitHub.com account to manage your #{config && config.companyName || 'corporate'} open source identity
else
small Sign in to GitHub
p
a.btn.btn-sm.btn-primary(href='/signin/github') Sign in
div.col-md-6
@ -98,16 +106,19 @@ html(lang="en")
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
if primaryAuthenticationScheme == 'github'
a.btn.btn-sm.btn-white(href='/link/update', style='margin-left:10px') Change
else
a.btn.btn-sm.btn-white(href=azureSignout, style='margin-left:10px') Sign Out
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
a.btn.btn-sm.btn-muted(href=azureSignout)= 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
a.btn.btn-sm.btn-white(href=azureSignout, 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
@ -151,6 +162,8 @@ html(lang="en")
h4
| Storage:&nbsp;
span.bg-danger(style='color:white;padding:2px')= config.azureStorage.account
h4
| Storage Prefix: #{config.azureStorage.prefix}
if config && config.redis
h4
| Redis:&nbsp;

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

@ -27,7 +27,7 @@ block content
tr
- gitAlertClass = config.primaryAuthenticationScheme == 'github' ? '' : 'alert-success'
- aadAlertClass = config.primaryAuthenticationScheme == 'aad' ? '' : 'alert-success'
td(class=gitAlertClass)= user.github.displayUsernameTemporary || user.github.username
td(class=gitAlertClass)= user.github.username
td
div.text-center
i.glyphicon.glyphicon-link

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

@ -17,7 +17,7 @@ block content
each column in linksForCleanupByColumn
div.col-sm-6.col-md-6.col-lg-6
each link in column
div(style='border:4px solid #ddd;padding:0 24px 0 16px')
div.horizontal-pad.vertical-space(style='border:4px solid #ddd')
div
h2= link.ghu
p GitHub Account
@ -25,11 +25,11 @@ block content
| View #{link.ghu} GitHub Profile &nbsp;
i.glyphicon.glyphicon-share-alt
form(method='post').vertical-space
input(type='hidden', name='unlink', value=link.ghu)
- var isConfirming = confirming === link.ghu
input(type='hidden', name='unlink', value=link.ghid)
- var isConfirming = confirming === link.ghid
- var buttonClass = isConfirming ? 'btn-danger' : 'btn-default'
if isConfirming
input(type='hidden', name='confirm', value=link.ghu)
input(type='hidden', name='confirm', value=link.ghid)
p.text-danger Please confirm that you are ready to unlink this GitHub account. It will lose access to any and all corporate organizations, teams and private repos.
input.btn.btn-lg(class=buttonClass, type='submit', value=isConfirming ? 'Yes, unlink #{link.ghu}' : 'Unlink #{link.ghu}')
if link.joinedDate

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

@ -32,10 +32,8 @@ block content
li Private forks of repos from these orgs, if any, will be removed by GitHub
li Work committed in a private fork of a #{config.companyName} org will be lost
p If you have any questions about this, please contact your team's GitHub liasons or your corporate legal contact.
form(method='post', action='/unlink')
p
input.btn.btn-lg.btn-primary(type='submit', value='Remove my corporate link')
input.btn.btn-lg.btn-danger(type='submit', value='Remove my corporate link')
| &nbsp; &nbsp; &nbsp;
a.btn.btn-lg.btn-default(href='/') Cancel