Added support for Google Sign-In, optional in every way.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3187 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
		| @@ -9,9 +9,10 @@ | |||||||
| 		<link type="text/css" rel="stylesheet" href="/terminal/style.css"></link> | 		<link type="text/css" rel="stylesheet" href="/terminal/style.css"></link> | ||||||
| 		<link type="image/png" rel="shortcut icon" href="/terminal/favicon.png"></link> | 		<link type="image/png" rel="shortcut icon" href="/terminal/favicon.png"></link> | ||||||
| 		<meta name="viewport" content="width=device-width, initial-scale=1"> | 		<meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
|  | 		<!--HEAD--> | ||||||
| 	</head> | 	</head> | ||||||
| 	<body> | 	<body> | ||||||
| 		<h1>Login</h1> | 		<h1>Login</h1> | ||||||
| 		<div id="content">$(SESSION)</div> | 		<div id="content"><!--SESSION--></div> | ||||||
| 	</body> | 	</body> | ||||||
| </html> | </html> | ||||||
|   | |||||||
							
								
								
									
										81
									
								
								core/auth.js
									
									
									
									
									
								
							
							
						
						
									
										81
									
								
								core/auth.js
									
									
									
									
									
								
							| @@ -8,6 +8,7 @@ var bCryptLib = require('bCrypt'); | |||||||
| bCrypt = new bCryptLib.bCrypt(); | bCrypt = new bCryptLib.bCrypt(); | ||||||
|  |  | ||||||
| var form = require('form'); | var form = require('form'); | ||||||
|  | var http = require('http'); | ||||||
|  |  | ||||||
| File.makeDirectory("data"); | File.makeDirectory("data"); | ||||||
| File.makeDirectory("data/auth"); | File.makeDirectory("data/auth"); | ||||||
| @@ -108,6 +109,7 @@ function authHandler(request, response) { | |||||||
| 					} | 					} | ||||||
| 				} else { | 				} else { | ||||||
| 					if (gAccounts[formData.name] && | 					if (gAccounts[formData.name] && | ||||||
|  | 						gAccounts[formData.name].password && | ||||||
| 						verifyPassword(formData.password, gAccounts[formData.name].password)) { | 						verifyPassword(formData.password, gAccounts[formData.name].password)) { | ||||||
| 						writeSession(session, {name: formData.name}); | 						writeSession(session, {name: formData.name}); | ||||||
| 						if (noAdministrator()) { | 						if (noAdministrator()) { | ||||||
| @@ -140,6 +142,44 @@ function authHandler(request, response) { | |||||||
| 				} | 				} | ||||||
| 				contents += '<div><a href="/login/logout">Logout</a></div>\n'; | 				contents += '<div><a href="/login/logout">Logout</a></div>\n'; | ||||||
| 			} else { | 			} else { | ||||||
|  | 				if (gGlobalSettings && gGlobalSettings['google-signin-client_id']) { | ||||||
|  | 					html = html.replace("<!--HEAD-->", ` | ||||||
|  | 		<script src="https://apis.google.com/js/platform.js" async defer></script> | ||||||
|  | 		<meta name="google-signin-client_id" content="${gGlobalSettings['google-signin-client_id']}"> | ||||||
|  | 		<script> | ||||||
|  | 			function onGoogleSignIn(user) { | ||||||
|  | 				var token = user.getAuthResponse().id_token; | ||||||
|  | 				var xhr = new XMLHttpRequest(); | ||||||
|  | 				xhr.open("POST", "/login/google"); | ||||||
|  | 				xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); | ||||||
|  | 				xhr.onload = function() { | ||||||
|  | 					if (xhr.status == 200) { | ||||||
|  | 						var redirected = false; | ||||||
|  | 						if (window.location.search.length) { | ||||||
|  | 							var query = window.location.search.substring(1); | ||||||
|  | 							var parts = query.split("&"); | ||||||
|  | 							for (var i = 0; i < parts.length; i++) { | ||||||
|  | 								var part = decodeURIComponent(parts[i]); | ||||||
|  | 								var key = part.substring(0, part.indexOf('=')); | ||||||
|  | 								var value = part.substring(part.indexOf('=') + 1); | ||||||
|  | 								if (key == "return") { | ||||||
|  | 									redirected = true; | ||||||
|  | 									window.location.href = value; | ||||||
|  | 								} | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 						if (!redirected) { | ||||||
|  | 							window.location.path = "/"; | ||||||
|  | 						} | ||||||
|  | 					} else { | ||||||
|  | 						alert(xhr.response); | ||||||
|  | 					} | ||||||
|  | 				}; | ||||||
|  | 				xhr.send('token=' + token); | ||||||
|  | 			} | ||||||
|  | 		</script> | ||||||
|  | 		`); | ||||||
|  | 				} | ||||||
| 				contents += '<form method="POST">\n'; | 				contents += '<form method="POST">\n'; | ||||||
| 				if (loginError) { | 				if (loginError) { | ||||||
| 					contents += "<p>" + loginError + "</p>\n"; | 					contents += "<p>" + loginError + "</p>\n"; | ||||||
| @@ -157,13 +197,17 @@ function authHandler(request, response) { | |||||||
| 				contents += '<div><input id="loginButton" type="submit" name="submit" value="Login"></input></div>\n'; | 				contents += '<div><input id="loginButton" type="submit" name="submit" value="Login"></input></div>\n'; | ||||||
| 				contents += '</div>'; | 				contents += '</div>'; | ||||||
| 				contents += '<div id="auth_or"> - or - </div>'; | 				contents += '<div id="auth_or"> - or - </div>'; | ||||||
|  | 				if (gGlobalSettings && gGlobalSettings['google-signin-client_id']) { | ||||||
|  | 					contents += '<div class="g-signin2" data-onsuccess="onGoogleSignIn" data-scope="profile"></div>'; | ||||||
|  | 					contents += '<div id="auth_or"> - or - </div>'; | ||||||
|  | 				} | ||||||
| 				contents += '<div id="auth_guest">\n'; | 				contents += '<div id="auth_guest">\n'; | ||||||
| 				contents += '<input id="guestButton" type="submit" name="submit" value="Proceeed as Guest"></input>\n'; | 				contents += '<input id="guestButton" type="submit" name="submit" value="Proceeed as Guest"></input>\n'; | ||||||
| 				contents += '</div>\n'; | 				contents += '</div>\n'; | ||||||
| 				contents += '</div>\n'; | 				contents += '</div>\n'; | ||||||
| 				contents += '</form>'; | 				contents += '</form>'; | ||||||
| 			} | 			} | ||||||
| 			var text = html.replace("$(SESSION)", contents); | 			var text = html.replace("<!--SESSION-->", contents); | ||||||
| 			response.writeHead(200, {"Content-Type": "text/html; charset=utf-6", "Set-Cookie": cookie, "Content-Length": text.length}); | 			response.writeHead(200, {"Content-Type": "text/html; charset=utf-6", "Set-Cookie": cookie, "Content-Length": text.length}); | ||||||
| 			response.end(text); | 			response.end(text); | ||||||
| 		} | 		} | ||||||
| @@ -171,12 +215,47 @@ function authHandler(request, response) { | |||||||
| 		removeSession(session); | 		removeSession(session); | ||||||
| 		response.writeHead(303, {"Set-Cookie": "session=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT", "Location": "/login" + (request.query ? "?" + request.query : "")}); | 		response.writeHead(303, {"Set-Cookie": "session=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT", "Location": "/login" + (request.query ? "?" + request.query : "")}); | ||||||
| 		response.end(); | 		response.end(); | ||||||
|  | 	} else if (request.uri == "/login/google") { | ||||||
|  | 		var formData = form.decodeForm(request.query, form.decodeForm(request.body)); | ||||||
|  | 		return verifyGoogleToken(formData.token).then(function(user) { | ||||||
|  | 			if (user && user.aud == gGlobalSettings['google-signin-client_id']) { | ||||||
|  | 				session = newSession(); | ||||||
|  | 				var userId = user.name; | ||||||
|  | 				if (gAccounts[userId] && !gAccounts[userId].google) { | ||||||
|  | 					response.writeHead(500, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"}); | ||||||
|  | 					response.end("Account already exists and is not a Google account."); | ||||||
|  | 				} else { | ||||||
|  | 					if (!gAccounts[userId]) { | ||||||
|  | 						gAccounts[userId] = {google: true}; | ||||||
|  | 						File.writeFile(kAccountsFile, JSON.stringify(gAccounts)); | ||||||
|  | 						if (noAdministrator()) { | ||||||
|  | 							makeAdministrator(userId); | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					writeSession(session, {name: userId, google: true}); | ||||||
|  |  | ||||||
|  | 					var cookie = "session=" + session + "; path=/; Max-Age=604800"; | ||||||
|  | 					response.writeHead(200, {"Content-Type": "text/json; charset=utf-8", "Connection": "close", "Set-Cookie": cookie}); | ||||||
|  | 					response.end(JSON.stringify(user)); | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				response.writeHead(500, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"}); | ||||||
|  | 				response.end(); | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
| 	} else { | 	} else { | ||||||
| 		response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"}); | 		response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"}); | ||||||
| 		response.end("Hello, " + request.client.peerName + "."); | 		response.end("Hello, " + request.client.peerName + "."); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function verifyGoogleToken(token) { | ||||||
|  | 	return http.get("https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=" + token).then(function(response) { | ||||||
|  | 		return JSON.parse(response.body); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  |  | ||||||
| function getPermissions(session) { | function getPermissions(session) { | ||||||
| 	var permissions; | 	var permissions; | ||||||
| 	var entry = readSession(session); | 	var entry = readSession(session); | ||||||
|   | |||||||
| @@ -266,7 +266,15 @@ function updateLogin() { | |||||||
| 	var a = document.createElement("a"); | 	var a = document.createElement("a"); | ||||||
| 	if (gCredentials && gCredentials.session) { | 	if (gCredentials && gCredentials.session) { | ||||||
| 		a.appendChild(document.createTextNode("logout " + gCredentials.session.name)); | 		a.appendChild(document.createTextNode("logout " + gCredentials.session.name)); | ||||||
| 		a.setAttribute("href", "/login/logout?return=" + encodeURIComponent(url())); | 		if (gCredentials.session.google) { | ||||||
|  | 			gapi.load("auth2", function() { | ||||||
|  | 				gapi.auth2.init(); | ||||||
|  | 			}); | ||||||
|  | 			a.setAttribute("onclick", "logoutGoogle()"); | ||||||
|  | 			a.setAttribute("href", "#"); | ||||||
|  | 		} else { | ||||||
|  | 			a.setAttribute("href", "/login/logout?return=" + encodeURIComponent(url())); | ||||||
|  | 		} | ||||||
| 	} else if (window.location.href.indexOf("?guest=1") != -1) { | 	} else if (window.location.href.indexOf("?guest=1") != -1) { | ||||||
| 		window.location.href = "/login?submit=Proceed+as+Guest&return=" + encodeURIComponent(url()); | 		window.location.href = "/login?submit=Proceed+as+Guest&return=" + encodeURIComponent(url()); | ||||||
| 	} else { | 	} else { | ||||||
| @@ -275,6 +283,12 @@ function updateLogin() { | |||||||
| 	login.appendChild(a); | 	login.appendChild(a); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function logoutGoogle() { | ||||||
|  | 	gapi.auth2.getAuthInstance().signOut().then(function() { | ||||||
|  | 		window.location.href = "/login/logout?return=" + encodeURIComponent(url()); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  |  | ||||||
| var gOriginalInput; | var gOriginalInput; | ||||||
| function dragHover(event) { | function dragHover(event) { | ||||||
| 	event.stopPropagation(); | 	event.stopPropagation(); | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								core/core.js
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								core/core.js
									
									
									
									
									
								
							| @@ -279,6 +279,17 @@ function getProcess(packageOwner, packageName, key, options) { | |||||||
| 						} | 						} | ||||||
| 						process.eventHandlers[eventName].push(handler); | 						process.eventHandlers[eventName].push(handler); | ||||||
| 					}, | 					}, | ||||||
|  | 					'unregister': function(eventHandle, handler) { | ||||||
|  | 						if (process.eventHandlers(eventName)) { | ||||||
|  | 							let index = process.eventHandlers[eventName].indexOf(handler); | ||||||
|  | 							if (index != -1) { | ||||||
|  | 								process.eventHandlers[eventName].splice(index, 1); | ||||||
|  | 							} | ||||||
|  | 							if (process.eventHandlers[eventName].length == 0) { | ||||||
|  | 								delete process.eventHandlers[eventName]; | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					}, | ||||||
| 					'getUser': getUser.bind(null, process, process), | 					'getUser': getUser.bind(null, process, process), | ||||||
| 					'user': getUser(process, process), | 					'user': getUser(process, process), | ||||||
| 				}, | 				}, | ||||||
|   | |||||||
							
								
								
									
										61
									
								
								core/http.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								core/http.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | function parseUrl(url) { | ||||||
|  | 	// XXX: Hack. | ||||||
|  | 	var match = url.match(new RegExp("(\\w+)://([^/]+)?(.*)")); | ||||||
|  | 	return { | ||||||
|  | 		protocol: match[1], | ||||||
|  | 		host: match[2], | ||||||
|  | 		path: match[3], | ||||||
|  | 		port: match[1] == "http" ? 80 : 443, | ||||||
|  | 	}; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function parseResponse(data) { | ||||||
|  | 	var firstLine; | ||||||
|  | 	var headers = {}; | ||||||
|  |  | ||||||
|  | 	while (true) { | ||||||
|  | 		var endLine = data.indexOf("\r\n"); | ||||||
|  | 		var line = data.substring(0, endLine); | ||||||
|  | 		if (!firstLine) { | ||||||
|  | 			firstLine = line; | ||||||
|  | 		} else if (!line.length) { | ||||||
|  | 			break; | ||||||
|  | 		} else { | ||||||
|  | 			var colon = line.indexOf(":"); | ||||||
|  | 			headers[line.substring(colon)] = line.substring(colon + 1); | ||||||
|  | 		} | ||||||
|  | 		data = data.substring(endLine + 2); | ||||||
|  | 	} | ||||||
|  | 	return {body: data}; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function get(url) { | ||||||
|  | 	var parsed = parseUrl(url); | ||||||
|  | 	return new Promise(function(resolve, reject) { | ||||||
|  | 		var socket = new Socket(); | ||||||
|  | 		var buffer = ""; | ||||||
|  |  | ||||||
|  | 		return socket.connect(parsed.host, parsed.port).then(function() { | ||||||
|  | 			socket.read(function(data) { | ||||||
|  | 				if (data) { | ||||||
|  | 					buffer += data; | ||||||
|  | 				} else { | ||||||
|  | 					resolve(parseResponse(buffer)); | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			if (parsed.port == 443) { | ||||||
|  | 				return socket.startTls(); | ||||||
|  | 			} | ||||||
|  | 		}).then(function() { | ||||||
|  | 			socket.write(`GET ${parsed.path} HTTP/1.0\r\nHost: ${parsed.host}\r\nConnection: close\r\n\r\n`); | ||||||
|  | 			socket.shutdown(); | ||||||
|  | 		}).catch(function(error) { | ||||||
|  | 			reject(error); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | exports.get = get; | ||||||
| @@ -4,6 +4,7 @@ | |||||||
| 		<link type="text/css" rel="stylesheet" href="/terminal/style.css"></link> | 		<link type="text/css" rel="stylesheet" href="/terminal/style.css"></link> | ||||||
| 		<link type="image/png" rel="shortcut icon" href="/terminal/favicon.png"></link> | 		<link type="image/png" rel="shortcut icon" href="/terminal/favicon.png"></link> | ||||||
| 		<meta name="viewport" content="width=device-width, initial-scale=1"> | 		<meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
|  | 		<!--HEAD--> | ||||||
| 	</head> | 	</head> | ||||||
| 	<body> | 	<body> | ||||||
| 		<div id="body"> | 		<div id="body"> | ||||||
|   | |||||||
| @@ -178,6 +178,11 @@ function handler(request, response, packageOwner, packageName, uri) { | |||||||
| 				found = true; | 				found = true; | ||||||
| 				var data = File.readFile("core/" + kStaticFiles[i].path); | 				var data = File.readFile("core/" + kStaticFiles[i].path); | ||||||
| 				if (kStaticFiles[i].uri == "") { | 				if (kStaticFiles[i].uri == "") { | ||||||
|  | 					if (gGlobalSettings && gGlobalSettings['google-signin-client_id']) { | ||||||
|  | 						data = data.replace("<!--HEAD-->", ` | ||||||
|  | 		<script src="https://apis.google.com/js/platform.js" async defer></script> | ||||||
|  | 		<meta name="google-signin-client_id" content="${gGlobalSettings['google-signin-client_id']}">`); | ||||||
|  | 					} | ||||||
| 					data = data.replace("$(VIEW_SOURCE)", "/~" + packageOwner + "/" + packageName + "/view"); | 					data = data.replace("$(VIEW_SOURCE)", "/~" + packageOwner + "/" + packageName + "/view"); | ||||||
| 					data = data.replace("$(EDIT_SOURCE)", "/~" + packageOwner + "/" + packageName + "/edit"); | 					data = data.replace("$(EDIT_SOURCE)", "/~" + packageOwner + "/" + packageName + "/edit"); | ||||||
| 				} else if (kStaticFiles[i].uri == "/edit") { | 				} else if (kStaticFiles[i].uri == "/edit") { | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ if (core.user.credentials.permissions && | |||||||
| 		], | 		], | ||||||
| 		[ | 		[ | ||||||
| 			["set ", {class: "cyan", value: "key value"}], | 			["set ", {class: "cyan", value: "key value"}], | ||||||
| 			["Set global setting key to value."], | 			["Set global setting key to value.  Omit value to unset."], | ||||||
| 		], | 		], | ||||||
| 		[ | 		[ | ||||||
| 			"permission list", | 			"permission list", | ||||||
| @@ -45,6 +45,7 @@ var kSimpleSettings = [ | |||||||
| 	'httpPort', | 	'httpPort', | ||||||
| 	'httpsPort', | 	'httpsPort', | ||||||
| 	'index', | 	'index', | ||||||
|  | 	'google-signin-client_id', | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| function printSettings(settings) { | function printSettings(settings) { | ||||||
| @@ -73,12 +74,16 @@ function onInput(input) { | |||||||
| 					terminal.print(" ".repeat(16 - s[i].toString().length), s[i].toString(), " ", i); | 					terminal.print(" ".repeat(16 - s[i].toString().length), s[i].toString(), " ", i); | ||||||
| 				} | 				} | ||||||
| 			}); | 			}); | ||||||
| 		} else if (match = /^\s*set\s+(\w+)\s+(.*)/.exec(input)) { | 		} else if (match = /^\s*set\s+(\S+)(?:\s+(.*))?/.exec(input)) { | ||||||
| 			var key = match[1]; | 			var key = match[1]; | ||||||
| 			var value = match[2]; | 			var value = match[2]; | ||||||
| 			administration.getGlobalSettings().then(function(settings) { | 			administration.getGlobalSettings().then(function(settings) { | ||||||
| 				if (kSimpleSettings.indexOf(key) != -1) { | 				if (kSimpleSettings.indexOf(key) != -1) { | ||||||
| 					settings[key] = value; | 					if (value) { | ||||||
|  | 						settings[key] = value; | ||||||
|  | 					} else { | ||||||
|  | 						delete settings[key]; | ||||||
|  | 					} | ||||||
| 					administration.setGlobalSettings(settings).then(function() { | 					administration.setGlobalSettings(settings).then(function() { | ||||||
| 						administration.getGlobalSettings().then(printSettings); | 						administration.getGlobalSettings().then(printSettings); | ||||||
| 					}).catch(function(error) { | 					}).catch(function(error) { | ||||||
|   | |||||||
| @@ -100,7 +100,10 @@ core.register("onInput", function(input) { | |||||||
| 	if (input == "new post") { | 	if (input == "new post") { | ||||||
| 		startNewPost(); | 		startNewPost(); | ||||||
| 	} else if (input == "submit") { | 	} else if (input == "submit") { | ||||||
| 		submitNewPost().then(renderBlog); | 		submitNewPost().then(function() { | ||||||
|  | 			core.unregister("onWindowMessage", onWindowMessage); | ||||||
|  | 			renderBlog(); | ||||||
|  | 		}); | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  |  | ||||||
| @@ -131,18 +134,19 @@ function submitNewPost() { | |||||||
| 	return gBlog.append(gNewPost); | 	return gBlog.append(gNewPost); | ||||||
| } | } | ||||||
|  |  | ||||||
| function startNewPost() { | function onWindowMessage(message) { | ||||||
| 	core.register("onWindowMessage", function(message) { | 	gNewPost = message.message; | ||||||
| 		gNewPost = message.message; | 	terminal.cork(); | ||||||
| 		terminal.cork(); | 	terminal.select("right"); | ||||||
| 		terminal.select("right"); | 	terminal.clear(); | ||||||
| 		terminal.clear(); | 	terminal.print({style: "font-width: x-large", value: message.message.title}); | ||||||
| 		terminal.print({style: "font-width: x-large", value: message.message.title}); | 	terminal.print(message.message.entry); | ||||||
| 		terminal.print(message.message.entry); | 	terminal.print({command: "submit"}); | ||||||
| 		terminal.print({command: "submit"}); | 	terminal.uncork(); | ||||||
| 		terminal.uncork(); | } | ||||||
| 	}); |  | ||||||
|  |  | ||||||
|  | function startNewPost() { | ||||||
|  | 	core.register("onWindowMessage", onWindowMessage); | ||||||
| 	terminal.split([ | 	terminal.split([ | ||||||
| 		{ | 		{ | ||||||
| 			type: "horizontal", | 			type: "horizontal", | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user