adding https and wikiDomains for login, plus GitHub and Google login, and switching to using winchan for communication between windows

This commit is contained in:
Paul Rodwell 2016-04-29 21:29:58 +01:00
parent f02c0e6334
commit a370af2f5f
9 changed files with 526 additions and 104 deletions

3
.gitignore vendored
View File

@ -2,3 +2,6 @@ node_modules
*.log
/client/*.js
/client/*.map
# don't ignore winchan.js
!/client/winchan.js

View File

@ -819,6 +819,35 @@ section>.contents {
z-index: 10;
}
/* Icon (github) */
.github-button span:after{
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAABBNpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIj4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ1M2IChNYWNpbnRvc2gpPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPnhtcC5kaWQ6RERCMUIwOUY4NkNFMTFFM0FBNTJFRTMzNTJEMUJDNDY8L3htcE1NOkRvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpEZXJpdmVkRnJvbSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgIDxzdFJlZjppbnN0YW5jZUlEPnhtcC5paWQ6RTUxNzhBMkE5OUEwMTFFMjlBMTVCQzEwNDZBODkwNEQ8L3N0UmVmOmluc3RhbmNlSUQ+CiAgICAgICAgICAgIDxzdFJlZjpkb2N1bWVudElEPnhtcC5kaWQ6RTUxNzhBMkI5OUEwMTFFMjlBMTVCQzEwNDZBODkwNEQ8L3N0UmVmOmRvY3VtZW50SUQ+CiAgICAgICAgIDwveG1wTU06RGVyaXZlZEZyb20+CiAgICAgICAgIDx4bXBNTTpJbnN0YW5jZUlEPnhtcC5paWQ6RERCMUIwOUU4NkNFMTFFM0FBNTJFRTMzNTJEMUJDNDY8L3htcE1NOkluc3RhbmNlSUQ+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgqBOjEgAAADH0lEQVRIDZWVzUuVQRTGu6kFJlFRBmKYfRhu+7CVVtCiNi6CPiDalVGgG1v0QdEfEi3bhLgIiigiqo0WbYI+FokStVAoWoVW3n6/cY5Nt0vYA897zpx5zjkz7zt3bmVZDarVakOlUvlpGL8Tsw8ehjvhJrgSzsIP8CW8Bx+TM4k1ZzHf8V9AsNwgthVeg+/hUqBOfWvOT3XqNahkQS/i8aLyfOHXc8t583pznVRvsRETsYN+/Jmi0nf8n3lssVo65fycTob5/bnRwo4INORAH340mMX/BgPRKMZhy7h684R1+nLdVN9vsBG+gAG3fRQOw1cRxH6Bn7KNsPMXoPryNVtvo40afYDzcBecgyvgOKflNtYFjGL2whk4DT15rs6PvAGOoZ3AqvUk7oHWsd45eN2JrXASini3N/EbYP1TQmYJdVlvnog61t3iTvbDDliFZdF5VmiCq3buz9NCAKQ4Or9NOW8d5zrgAXcyYiUQH3EaP45h2RR9faBPOvOg+SLqjdhkIoWqVY+ruGMpbLmy+tWLaOjNhyLqTbiC9qyNVX+sGRel/ulG/qea/HYnmnLQdyhqxwvRpT9r85ts4mUnokm6fxZC//WMfI+1iPGsTaZS6Pfp6eJ9dnJi5rHxO8qS+kZd1nei2FFTb8omYzmob/cueNoYiT8o4G+g7iEw7rw69eAM3A7nofXEuKfoBBSuXApPxmW41J00ob0C474ra52wSRuM++kW/jD8DMUzeBEehM0uC5t2hV0FD8GrcAwGbBC/Eeu2mWfiYFZ8xR6HXvklnjJYm7Vxa68j9qgU4Vu8bDKUGuTEFibv5oQp7GZ4DD6Bz+GRrPMbpO+Qx5cYC694i4v4Ed7Hb0lNcGJl3fivobiRizTjr07C4kEscs4qBh4Qm2jFO9idayStryudBOxu+BYKX9EQ9L+iJyfE94gm55gT7iTwBsfrfrGufgIT0cirfzQysh1UhB/Fw56v0XnZbsvaOMKp/uIDQTRqxD8JH0AxkBOjeNiBhenqQ+wpmK4UbP0G0akU4K+BPXB9bhKvK+x65vbCdPKy5q8GvwD3BqLoPnaIbQAAAABJRU5ErkJggg==) 4px center no-repeat;
content: '';
display: block;
width: 31px;
position: absolute;
bottom: 0;
left: -3px;
top: 0;
z-index: 10;
}
/* Icon (google) */
.google-button span:after{
background: url(data:image/png;base64,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) 4px center no-repeat;
background-size: 18px;
content: "";
display: block;
width: 31px;
position: absolute;
bottom: 0;
left: 0;
top: 0;
z-index: 10;
}
/* Icon background */
.scheme-button span:before{
content: '';
@ -846,6 +875,10 @@ section>.contents {
border-radius: 3px 0 0 3px;
}
.google-button span:before{
background: white;
}
/* Triangle */
.scheme-button:before{
background: #42a9dd;
@ -879,6 +912,11 @@ section>.contents {
transform: rotate(45deg);
}
.google-button:before{
background: white;
}
/* Inset shadow (required here because the icon background clips it when on the `a` element) */
.scheme-button:after{
content: '';

10
client/relay.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<script>
function doPost(msg, origin) {
window.parent.postMessage(msg, origin);
}
</script>
</html>

View File

@ -13,6 +13,8 @@
###
settings = {}
claim_wiki = () ->
# we want to initiate a claim on a wiki
#
@ -31,7 +33,6 @@ claim_wiki = () ->
response.json().then (json) ->
ownerName = json.ownerName
update_footer ownerName, true, true
console.log 'owner: ', json.ownerName, ' : ', ownerName
else
console.log 'Attempt to claim site failed', response
@ -72,21 +73,53 @@ update_footer = (ownerName, isAuthenticated, isOwner) ->
$('footer > #security').append "<a href='#' id='show-security-dialog' class='footer-item' title='#{signonTitle}'><i class='fa fa-lock fa-lg fa-fw'></i></a>"
$('footer > #security > #show-security-dialog').click (e) ->
e.preventDefault()
securityDialog = window.open(
"/auth/loginDialog",
"_blank",
"width=700, height=375, menubar=no, location=no, chrome=yes, centerscreen")
securityDialog.window.focus()
w = WinChan.open({
url: settings.dialogURL
relay_url: settings.relayURL
window_features: "menubar=0, location=0, resizable=0, scrollbars=0, status=0, dialog=1, width=700, height=375"
params: {}
}, (err, r) ->
if err
console.log err
else if !isClaimed
claim_wiki()
else
update_footer ownerName, true)
setup = (user) ->
# we will replace font-awesome with a small number of svg icons at a later date...
if (!$("link[href='https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css']").length)
$('<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">').appendTo("head")
wiki.getScript '/security/winchan.js'
if (!$("link[href='/security/style.css']").length)
$('<link rel="stylesheet" href="/security/style.css">').appendTo("head")
myInit = {
method: 'GET'
cache: 'no-cache'
mode: 'same-origin'
}
fetch '/auth/client-settings.json', myInit
.then (response) ->
if response.ok
response.json().then (json) ->
settings = json
if settings.useHttps
dialogProtocol = 'https:'
else
dialogProtocol = window.location.protocol
if settings.wikiHost
dialogHost = settings.wikiHost
else
dialogHost = window.location.host
settings.dialogURL = dialogProtocol + '//' + dialogHost + '/auth/loginDialog'
settings.relayURL = dialogProtocol + '//' + dialogHost + '/auth/relay.html'
update_footer ownerName, isAuthenticated, isOwner
update_footer ownerName, isAuthenticated, isOwner
else
console.log 'Unable to fetch client settings: ', response
window.plugins.security = {setup, claim_wiki, update_footer}

301
client/winchan.js Normal file
View File

@ -0,0 +1,301 @@
var WinChan = (function() {
var RELAY_FRAME_NAME = "__winchan_relay_frame";
var CLOSE_CMD = "die";
// a portable addListener implementation
function addListener(w, event, cb) {
if(w.attachEvent) w.attachEvent('on' + event, cb);
else if (w.addEventListener) w.addEventListener(event, cb, false);
}
// a portable removeListener implementation
function removeListener(w, event, cb) {
if(w.detachEvent) w.detachEvent('on' + event, cb);
else if (w.removeEventListener) w.removeEventListener(event, cb, false);
}
// checking for IE8 or above
function isInternetExplorer() {
var rv = -1; // Return value assumes failure.
var ua = navigator.userAgent;
if (navigator.appName === 'Microsoft Internet Explorer') {
var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
if (re.exec(ua) != null)
rv = parseFloat(RegExp.$1);
}
// IE > 11
else if (ua.indexOf("Trident") > -1) {
var re = new RegExp("rv:([0-9]{2,2}[\.0-9]{0,})");
if (re.exec(ua) !== null) {
rv = parseFloat(RegExp.$1);
}
}
return rv >= 8;
}
// checking Mobile Firefox (Fennec)
function isFennec() {
try {
// We must check for both XUL and Java versions of Fennec. Both have
// distinct UA strings.
var userAgent = navigator.userAgent;
return (userAgent.indexOf('Fennec/') != -1) || // XUL
(userAgent.indexOf('Firefox/') != -1 && userAgent.indexOf('Android') != -1); // Java
} catch(e) {}
return false;
}
// feature checking to see if this platform is supported at all
function isSupported() {
return (window.JSON && window.JSON.stringify &&
window.JSON.parse && window.postMessage);
}
// given a URL, extract the origin. Taken from: https://github.com/firebase/firebase-simple-login/blob/d2cb95b9f812d8488bdbfba51c3a7c153ba1a074/js/src/simple-login/transports/WinChan.js#L25-L30
function extractOrigin(url) {
if (!/^https?:\/\//.test(url)) url = window.location.href;
var m = /^(https?:\/\/[\-_a-zA-Z\.0-9:]+)/.exec(url);
if (m) return m[1];
return url;
}
// find the relay iframe in the opener
function findRelay() {
var loc = window.location;
var frames = window.opener.frames;
for (var i = frames.length - 1; i >= 0; i--) {
try {
if (frames[i].location.protocol === window.location.protocol &&
frames[i].location.host === window.location.host &&
frames[i].name === RELAY_FRAME_NAME)
{
return frames[i];
}
} catch(e) { }
}
return;
}
var isIE = isInternetExplorer();
if (isSupported()) {
/* General flow:
* 0. user clicks
* (IE SPECIFIC) 1. caller adds relay iframe (served from trusted domain) to DOM
* 2. caller opens window (with content from trusted domain)
* 3. window on opening adds a listener to 'message'
* (IE SPECIFIC) 4. window on opening finds iframe
* 5. window checks if iframe is "loaded" - has a 'doPost' function yet
* (IE SPECIFIC5) 5a. if iframe.doPost exists, window uses it to send ready event to caller
* (IE SPECIFIC5) 5b. if iframe.doPost doesn't exist, window waits for frame ready
* (IE SPECIFIC5) 5bi. once ready, window calls iframe.doPost to send ready event
* 6. caller upon reciept of 'ready', sends args
*/
return {
open: function(opts, cb) {
if (!cb) throw "missing required callback argument";
// test required options
var err;
if (!opts.url) err = "missing required 'url' parameter";
if (!opts.relay_url) err = "missing required 'relay_url' parameter";
if (err) setTimeout(function() { cb(err); }, 0);
// supply default options
if (!opts.window_name) opts.window_name = null;
if (!opts.window_features || isFennec()) opts.window_features = undefined;
// opts.params may be undefined
var iframe;
// sanity check, are url and relay_url the same origin?
var origin = extractOrigin(opts.url);
if (origin !== extractOrigin(opts.relay_url)) {
return setTimeout(function() {
cb('invalid arguments: origin of url and relay_url must match');
}, 0);
}
var messageTarget;
if (isIE) {
// first we need to add a "relay" iframe to the document that's served
// from the target domain. We can postmessage into a iframe, but not a
// window
iframe = document.createElement("iframe");
// iframe.setAttribute('name', framename);
iframe.setAttribute('src', opts.relay_url);
iframe.style.display = "none";
iframe.setAttribute('name', RELAY_FRAME_NAME);
document.body.appendChild(iframe);
messageTarget = iframe.contentWindow;
}
var w = opts.popup || window.open(opts.url, opts.window_name, opts.window_features);
if (opts.popup) {
w.location.href = opts.url;
}
if (!messageTarget) messageTarget = w;
// lets listen in case the window blows up before telling us
var closeInterval = setInterval(function() {
if (w && w.closed) {
cleanup();
if (cb) {
cb('User closed the popup window');
cb = null;
}
}
}, 500);
var req = JSON.stringify({a: 'request', d: opts.params});
// cleanup on unload
function cleanup() {
if (iframe) document.body.removeChild(iframe);
iframe = undefined;
if (closeInterval) closeInterval = clearInterval(closeInterval);
removeListener(window, 'message', onMessage);
removeListener(window, 'unload', cleanup);
if (w) {
try {
w.close();
} catch (securityViolation) {
// This happens in Opera 12 sometimes
// see https://github.com/mozilla/browserid/issues/1844
messageTarget.postMessage(CLOSE_CMD, origin);
}
}
w = messageTarget = undefined;
}
addListener(window, 'unload', cleanup);
function onMessage(e) {
if (e.origin !== origin) { return; }
try {
var d = JSON.parse(e.data);
if (d.a === 'ready') messageTarget.postMessage(req, origin);
else if (d.a === 'error') {
cleanup();
if (cb) {
cb(d.d);
cb = null;
}
} else if (d.a === 'response') {
cleanup();
if (cb) {
cb(null, d.d);
cb = null;
}
}
} catch(err) { }
}
addListener(window, 'message', onMessage);
return {
close: cleanup,
focus: function() {
if (w) {
try {
w.focus();
} catch (e) {
// IE7 blows up here, do nothing
}
}
}
};
},
onOpen: function(cb) {
var o = "*";
var msgTarget = isIE ? findRelay() : window.opener;
if (!msgTarget) throw "can't find relay frame";
function doPost(msg) {
msg = JSON.stringify(msg);
if (isIE) msgTarget.doPost(msg, o);
else msgTarget.postMessage(msg, o);
}
function onMessage(e) {
// only one message gets through, but let's make sure it's actually
// the message we're looking for (other code may be using
// postmessage) - we do this by ensuring the payload can
// be parsed, and it's got an 'a' (action) value of 'request'.
var d;
try {
d = JSON.parse(e.data);
} catch(err) { }
if (!d || d.a !== 'request') return;
removeListener(window, 'message', onMessage);
o = e.origin;
if (cb) {
// this setTimeout is critically important for IE8 -
// in ie8 sometimes addListener for 'message' can synchronously
// cause your callback to be invoked. awesome.
setTimeout(function() {
cb(o, d.d, function(r) {
cb = undefined;
doPost({a: 'response', d: r});
});
}, 0);
}
}
function onDie(e) {
if (e.data === CLOSE_CMD) {
try { window.close(); } catch (o_O) {}
}
}
addListener(isIE ? msgTarget : window, 'message', onMessage);
addListener(isIE ? msgTarget : window, 'message', onDie);
// we cannot post to our parent that we're ready before the iframe
// is loaded. (IE specific possible failure)
try {
doPost({a: "ready"});
} catch(e) {
// this code should never be exectued outside IE
addListener(msgTarget, 'load', function(e) {
doPost({a: "ready"});
});
}
// if window is unloaded and the client hasn't called cb, it's an error
var onUnload = function() {
try {
// IE8 doesn't like this...
removeListener(isIE ? msgTarget : window, 'message', onDie);
} catch (ohWell) { }
if (cb) doPost({ a: 'error', d: 'client closed window' });
cb = undefined;
// explicitly close the window, in case the client is trying to reload or nav
try { window.close(); } catch (e) { }
};
addListener(window, 'unload', onUnload);
return {
detach: function() {
removeListener(window, 'unload', onUnload);
}
};
}
};
} else {
return {
open: function(url, winopts, arg, cb) {
setTimeout(function() { cb("unsupported browser"); }, 0);
},
onOpen: function(cb) {
setTimeout(function() { cb("unsupported browser"); }, 0);
}
};
}
})();
if (typeof module !== 'undefined' && module.exports) {
module.exports = WinChan;
}

View File

@ -9,8 +9,11 @@
"license": "MIT",
"dependencies": {
"coffee-script": "1.10",
"lodash": "4",
"passport": "^0.3.2",
"passport-twitter": "*",
"passport-github": "*",
"passport-google-oauth20": "*",
"qs": "6.1"
},
"devDependencies": {

View File

@ -13,6 +13,10 @@ path = require 'path'
https = require 'https'
qs = require 'qs'
url = require 'url'
_ = require('lodash')
passport = require 'passport'
@ -27,6 +31,7 @@ module.exports = exports = (log, loga, argv) ->
ownerName = ''
user = {}
wikiName = argv.url
wikiHost = argv.wiki_domain
admin = argv.admin
@ -39,9 +44,19 @@ module.exports = exports = (log, loga, argv) ->
personaIDFile = argv.id
usingPersona = false
ids = {}
if argv.security_useHttps
useHttps = true
callbackProtocol = "https:"
else
useHttps = false
callbackProtocol = url.parse(argv.url).protocol
schemes = {}
if wikiHost
callbackHost = wikiHost
else
callbackHost = url.parse(argv.url).host
ids = []
# Mozilla Persona service closes on
personaEnd = new Date('2016-11-30')
@ -114,13 +129,12 @@ module.exports = exports = (log, loga, argv) ->
# site not claimed?
return true
else
authorized = false
try
authorized = switch req.session.passport.user.provider
when "twitter"
if owner.twitter.id is req.session.passport.user.id
true
return authorized
if owner[req.session.passport.user.provider].id is req.session.passport.user.id
return true
else
return false
return false
security.isAdmin = (req) ->
@ -128,13 +142,12 @@ module.exports = exports = (log, loga, argv) ->
# not added legacy support yet, so...
return false
else
admin = false
try
admin = switch req.session.passport.user.provider
when "twitter"
if admin is req.session.passport.user.id
true
return admin
if admin is req.session.passport.user.id
return true
else
return false
return false
security.login = (updateOwner) ->
console.log "Login...."
@ -151,111 +164,115 @@ module.exports = exports = (log, loga, argv) ->
passport.deserializeUser = (obj, req, done) ->
done(null, obj)
###
# Github Strategy
if argv.github_clientID? and argv.github_clientSecret?
github = {}
github['clientID'] = argv.github_clientID
github['clientSecret'] = argv.github_clientSecret
ids['github'] = github
ids.push('github')
GithubStrategy = require('passport-github').Strategy
passport.use(new GithubStrategy({
clientID: ids['github'].clientID
clientSecret: ids['github'].clientSecret
# this is not going to work - callback must equal that specified won github
# when the application was setup - it can't be dynamic....
callbackURL: 'http://localhost:3000/auth/github/callback'
}, (accessToken, refreshToken, profile, cb) ->
User.findOrCreate({githubID: profile.id}, (err, user) ->
return cb(err, user))))
###
if argv.twitter_consumerKey? and argv.twitter_consumerSecret?
schemes['twitter'] = true
twitter = {}
twitter['consumerKey'] = argv.twitter_consumerKey
twitter['consumerSecret'] = argv.twitter_consumerSecret
ids['twitter'] = twitter
TwitterStrategy = require('passport-twitter').Strategy
passport.use(new TwitterStrategy({
consumerKey: ids['twitter'].consumerKey
consumerSecret: ids['twitter'].consumerSecret
callbackURL: '/auth/twitter/callback'
clientID: argv.github_clientID
clientSecret: argv.github_clientSecret
scope: 'user:emails'
# callbackURL is optional, and if it exists must match that given in
# the OAuth application settings - so we don't specify it.
}, (accessToken, refreshToken, profile, cb) ->
user = {
"provider": 'twitter',
"id": profile.id,
"username": profile.username,
"displayName": profile.displayName
provider: 'github',
id: profile.id
username: profile.username
displayName: profile.displayName
email: profile.emails[0].value
}
cb(null, user)))
###
if argv.google_clientID? and argv.google_clientSecret?
google = {}
google['clientID'] = argv.google_clientID
google['clientSecret'] = argv.google_clientSecret
ids['google'] = google
# Twitter Strategy
if argv.twitter_consumerKey? and argv.twitter_consumerSecret?
ids.push('twitter')
TwitterStrategy = require('passport-twitter').Strategy
passport.use(new TwitterStrategy({
consumerKey: argv.twitter_consumerKey
consumerSecret: argv.twitter_consumerSecret
callbackURL: callbackProtocol + '//' + callbackHost + '/auth/twitter/callback'
}, (accessToken, refreshToken, profile, cb) ->
user = {
provider: 'twitter',
id: profile.id,
username: profile.username,
displayName: profile.displayName
}
cb(null, user)))
# Google Strategy
if argv.google_clientID? and argv.google_clientSecret?
ids.push('google')
GoogleStrategy = require('passport-google-oauth20').Strategy
passport.use(new GoogleStrategy({
clientID: ids['google'].clientID
clientSecret: ids['google'].clientSecret
callbackURL: 'http://localhost:3000/auth/google/callback'
clientID: argv.google_clientID
clientSecret: argv.google_clientSecret
callbackURL: callbackProtocol + '//' + callbackHost + '/auth/google/callback'
}, (accessToken, refreshToken, profile, cb) ->
user = {
provider: "google"
id: profile.id
displayName: profile.displayName
emails: profile.emails
}
cb(null, profile)))
###
app.use(passport.initialize())
app.use(passport.session())
### Github
app.get('/auth/github', passport.authenticate('github'), (req, res) -> )
# Github
app.get('/auth/github', passport.authenticate('github', {scope: 'user:email'}), (req, res) -> )
app.get('/auth/github/callback',
passport.authenticate('github', { failureRedirect: '/'}), (req, res) ->
# do what ever happens on login
)
###
passport.authenticate('github', { successRedirect: '/auth/loginDone', failureRedirect: '/auth/loginDialog'}))
# Twitter
app.get('/auth/twitter', passport.authenticate('twitter'), (req, res) -> )
app.get('/auth/twitter/callback',
passport.authenticate('twitter', { successRedirect: '/auth/loginDone', failureRedirect: '/auth/loginDialog'}))
### Google
# Google
app.get('/auth/google', passport.authenticate('google', { scope: [
'https://www.googleapis.com/auth/plus.profile.emails.read'
]}))
app.get('/auth/google/callback',
passport.authenticate('google', {failureRedirect: '/'}), (req, res) ->
console.log 'google logged in!!!!'
res.redirect('/view/welcome-visitors'))
###
passport.authenticate('google', { successRedirect: '/auth/loginDone', failureRedirect: '/auth/loginDialog'}))
app.get '/auth/client-settings.json', (req, res) ->
# the client needs some information to configure itself
settings = {
useHttps: useHttps
}
if wikiHost
settings.wikiHost = wikiHost
res.json settings
app.get '/auth/loginDialog', (req, res) ->
referer = req.headers.referer
console.log "logging into: ", url.parse(referer).hostname
console.log 'owner: ', owner
schemeButtons = []
_(ids).forEach (scheme) ->
console.log "Scheme: ", scheme
switch scheme
when "twitter" then schemeButtons.push({button: "<a href='/auth/twitter' class='scheme-button twitter-button'><span>Twitter</span></a>"})
when "github" then schemeButtons.push({button: "<a href='/auth/github' class='scheme-button github-button'><span>Github</span></a>"})
when "google" then schemeButtons.push({button: "<a href='/auth/google' class='scheme-button google-button'><span>Google</span></a>"})
info = {
wikiName: req.hostname
wikiHostName: "a federated wiki site"
title: if owner
"Federated Wiki: Site Owner Sign-on"
wikiName: url.parse(referer).hostname
wikiHostName: if wikiHost
"part of " + req.hostname + " wiki farm"
else
"Federated Wiki: Claim Wiki Site"
loginText: if owner
"Sign in to"
else
"Claim wiki"
schemes: "<a href='/auth/twitter' class='scheme-button twitter-button'><span>Twitter</span></a>"
"a federated wiki site"
title: "Federated Wiki: Site Owner Sign-on"
loginText: "Sign in to"
schemes: schemeButtons
}
res.render(path.join(__dirname, '..', 'views', 'securityDialog.html'), info)
@ -276,6 +293,7 @@ module.exports = exports = (log, loga, argv) ->
res.sendStatus(403)
else
user = req.session.passport.user
console.log "Claim: user = ", user
id = switch user.provider
when "twitter" then {
name: user.displayName
@ -284,10 +302,25 @@ module.exports = exports = (log, loga, argv) ->
username: user.username
}
}
when "github" then {
name: user.displayName
github: {
id: user.id
username: user.username
email: user.email
}
}
when "google" then {
name: user.displayName
google: {
id: user.id
emails: user.emails
}
}
setOwner id, (err) ->
if err
console.log 'Failed to claim wiki ', req.hostname, ' for ', id
console.log 'Failed to claim wiki ', req.hostname, ' for ', JSON.stringify(id)
res.sendStatus(500)
updateOwner getOwner()
res.json({
@ -301,8 +334,4 @@ module.exports = exports = (log, loga, argv) ->
req.logout()
res.send("OK")
security

View File

@ -4,13 +4,6 @@
<title>Federated Wiki: Sign-on</title>
<link id='favicon' href='/favicon.png' rel='icon' type='image/png'>
<link rel="stylesheet" href="/security/style.css">
<script language='javascript' type='text/javascript'>
if (window.opener.isClaimed == false)
window.opener.plugins.security.claim_wiki()
else
window.opener.plugins.security.update_footer(window.opener.ownerName, true)
window.close()
</script>
</head>
<body>
@ -22,4 +15,14 @@
{{{authMessage}}}
</div>
</body>
<script src="/security/winchan.js"></script>
<script>
var wc = WinChan.onOpen(function(origin, r, cb) {
cb({
done: true,
timestamp: new Date().toString()
});
window.close();
})
</script>
</html>

View File

@ -12,7 +12,7 @@
<div class="wikiinfo">
<div class="table">
<div class="vertical">
<img id="wiki_logo" src="/favicon.png" width="32px" height="32px"></img>
<img id="wiki_logo" src="//{{wikiName}}/favicon.png" width="32px" height="32px"></img>
<h2 id=wiki_name>{{wikiName}}</h2>
<h3 id="wiki_hostname">{{wikiHostName}}</h3>
</div>
@ -24,7 +24,9 @@
<div class="contents">
<div class="scheme_section vcenter" style="width: 249px;">
<h2>{{loginText}} {{wikiName}} with...</h2>
{{{schemes}}}
{{#schemes}}
<p>{{{button}}}</p>
{{/schemes}}
</div>
</div>
</div>