changeset 3:2c63b10781e1

add login
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 24 Oct 2024 21:43:44 -0600
parents ee1f91e67509
children 2da10ece826f
files src/account.html.luan src/do_login.html.luan src/error_log.js.luan src/index.html.luan src/lib/Shared.luan src/lib/User.luan src/lib/Utils.luan src/login.html.luan src/login.js.luan src/login_sent.html.luan src/private/tools/config.html.luan src/private/tools/index.html.luan src/site.css src/site.js
diffstat 14 files changed, 344 insertions(+), 10 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/account.html.luan	Thu Oct 24 21:43:44 2024 -0600
@@ -0,0 +1,27 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local header = Shared.header or error()
+
+
+return function()
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html>
+	<head>
+<%		head() %>
+	</head>
+	<body>
+<%		header() %>
+		<div content>
+			<h1>Your Account</h1>
+			<p><a href="javascript:logout()">Logout</a></p>
+		</div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/do_login.html.luan	Thu Oct 24 21:43:44 2024 -0600
@@ -0,0 +1,40 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local header = Shared.header or error()
+local User = require "site:/lib/User.luan"
+
+
+return function()
+	local user_id = Http.request.parameters.user or error()
+	local password = Http.request.parameters.password or error()
+	local user = User.get_by_id(user_id)
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html>
+	<head>
+<%		head() %>
+	</head>
+	<body>
+<%		header() %>
+		<div content>
+			<h1>Login / Register</h1>
+<%	if user == nil or user.password ~= password then %>
+			<p>Login failed</p>
+<%
+	else
+		user.login()
+%>
+			<script> location = '/'; </script>
+<%
+	end
+%>
+		</div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/error_log.js.luan	Thu Oct 24 21:43:44 2024 -0600
@@ -0,0 +1,23 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local String = require "luan:String.luan"
+local trim = String.trim or error()
+local regex = String.regex or error()
+local contains = String.contains or error()
+local Http = require "luan:http/Http.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "error_log.js"
+
+
+local function priority(err)
+	return "error"
+end
+
+return function()
+	local err = Http.request.parameters.err
+	if err == nil then
+		return  -- stupid bots
+	end
+	local call = priority(err)
+	logger[call](trim(err).."\n"..trim(Http.request.raw_head).."\n")
+end
--- a/src/index.html.luan	Thu Oct 24 15:37:35 2024 -0600
+++ b/src/index.html.luan	Thu Oct 24 21:43:44 2024 -0600
@@ -14,7 +14,6 @@
 <html>
 	<head>
 <%		head() %>
-		<title>Web Chat</title>
 		<style>
 			h1 {
 				margin-bottom: 0;
--- a/src/lib/Shared.luan	Thu Oct 24 15:37:35 2024 -0600
+++ b/src/lib/Shared.luan	Thu Oct 24 21:43:44 2024 -0600
@@ -4,18 +4,26 @@
 local parse = Luan.parse or error()
 local Io = require "luan:Io.luan"
 local uri = Io.uri or error()
+local Time = require "luan:Time.luan"
+local Thread = require "luan:Thread.luan"
+local thread_run = Thread.run or error()
+local Mail = require "luan:mail/Mail.luan"
 local User = require "site:/lib/User.luan"
 local current_user = User.current or error()
 
 
 local Shared = {}
 
+local started = Time.now()
+
 function Shared.head()
 %>
 		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<title>Web Chat</title>
 		<style>
-			@import "/site.css";
+			@import "/site.css?s=<%=started%>";
 		</style>
+		<script src="/site.js?s=<%=started%>"></script>
 <%
 end
 
@@ -66,4 +74,12 @@
 	end
 end
 
+local send_mail = Mail.sender(Shared.config.mail_server).send
+
+function Shared.send_mail_async(mail)
+	thread_run( function()
+		send_mail(mail)
+	end )
+end
+
 return Shared
--- a/src/lib/User.luan	Thu Oct 24 15:37:35 2024 -0600
+++ b/src/lib/User.luan	Thu Oct 24 21:43:44 2024 -0600
@@ -1,13 +1,21 @@
 local Luan = require "luan:Luan.luan"
 local error = Luan.error
 local ipairs = Luan.ipairs or error()
+local range = Luan.range or error()
 local to_string = Luan.to_string or error()
 local get_local_only = Luan.get_local_only or error()
 local set_local_only = Luan.set_local_only or error()
+local String = require "luan:String.luan"
+local sub_string = String.sub or error()
+local Math = require "luan:Math.luan"
+local random = Math.random or error()
+local Table = require "luan:Table.luan"
+local concat = Table.concat or error()
 local Lucene = require "luan:lucene/Lucene.luan"
 local lucene_quote = Lucene.quote or error()
 local Http = require "luan:http/Http.luan"
 local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
 local Logging = require "luan:logging/Logging.luan"
 local logger = Logging.logger "User"
 
@@ -61,9 +69,38 @@
 end
 User.get_by_id = get_by_id
 
-function User.get_by_email(email)
-	local doc = Db.get_document("user_email:"..lucene_quote(email))
-	return doc and from_doc(doc)
+local password_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+do
+	local t = {}
+	for i in range(1,#password_chars) do
+		t[#t+1] = sub_string(password_chars,i,i)
+	end
+	password_chars = t
+end	
+
+local function new_password()
+	local n = #password_chars
+	local t = {}
+	for _ in range(1,10) do
+		t[#t+1] = password_chars[random(n)]
+	end
+	return concat(t)
+end
+
+function User.get_or_create_by_email(email)
+	return run_in_transaction( function()
+		local doc = Db.get_document("user_email:"..lucene_quote(email))
+		if doc ~= nil then
+			return from_doc(doc)
+		else
+			local user = User.new{
+				email = email
+				password = new_password()
+			}
+			user.save()
+			return user
+		end
+	end )
 end
 
 function User.search(query,sort,rows)
@@ -76,7 +113,7 @@
 	return users
 end
 
-function User.current()
+local function current()
 	local user = get_local_only(User,"current")
 	if user == nil then
 		local id = Http.request.cookies.user
@@ -85,7 +122,7 @@
 			user = "nil"
 		else
 			user = get_by_id(id)
-			if user == nil or user.registered == nil or user.password ~= password then
+			if user == nil or user.password ~= password then
 				user = "nil"
 			end
 		end
@@ -93,9 +130,10 @@
 	end
 	return user ~= "nil" and user or nil
 end
+User.current = current
 
 function User.current_required()
-	local user = User.current()
+	local user = current()
 	user or Http.response.send_redirect "/login.html"
 	return user
 end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/Utils.luan	Thu Oct 24 21:43:44 2024 -0600
@@ -0,0 +1,13 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Http = require "luan:http/Http.luan"
+
+
+local Utils = {}
+
+function Utils.base_url()
+	local request = Http.request
+	return request.scheme.."://"..request.headers["Host"]
+end
+
+return Utils
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/login.html.luan	Thu Oct 24 21:43:44 2024 -0600
@@ -0,0 +1,36 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local header = Shared.header or error()
+
+
+return function()
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html>
+	<head>
+<%		head() %>
+	</head>
+	<body>
+<%		header() %>
+		<div content>
+			<h1>Login / Register</h1>
+			<p>A link to login will be emailed to you.</p>
+			<form page onsubmit="ajaxForm('/login.js',this)" action="javascript:">
+				<p>
+					<label prompt>Your email address</label>
+					<input type=email name=email required autofocus>
+				</p>
+				<p>
+					<input type=submit>
+				</p>
+			</form>
+		</div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/login.js.luan	Thu Oct 24 21:43:44 2024 -0600
@@ -0,0 +1,30 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local send_mail_async = Shared.send_mail_async or error()
+local Utils = require "site:/lib/Utils.luan"
+local base_url = Utils.base_url or error()
+local User = require "site:/lib/User.luan"
+
+
+return function()
+	local email = Http.request.parameters.email or error()
+	local user = User.get_or_create_by_email(email)
+	local url = base_url().."/do_login.html?user="..user.id.."&password="..user.password
+	send_mail_async {
+		From = "Web Chat <chat@reactionary.software>"
+		To = email
+		Subject = "Login"
+		body = `%>
+Here is the link to login:
+
+<%= url %>
+<%		`
+	}
+	Io.stdout = Http.response.text_writer()
+%>
+	location = '/login_sent.html';
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/login_sent.html.luan	Thu Oct 24 21:43:44 2024 -0600
@@ -0,0 +1,27 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local header = Shared.header or error()
+
+
+return function()
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html>
+	<head>
+<%		head() %>
+	</head>
+	<body>
+<%		header() %>
+		<div content>
+			<h1>Login / Register</h1>
+			<p>A link to login has been emailed to you.</p>
+		</div>
+	</body>
+</html>
+<%
+end
--- a/src/private/tools/config.html.luan	Thu Oct 24 15:37:35 2024 -0600
+++ b/src/private/tools/config.html.luan	Thu Oct 24 21:43:44 2024 -0600
@@ -16,7 +16,6 @@
 <html>
 	<head>
 <%		head() %>
-		<title>Chat Private Tools</title>
 		<style>
 			textarea {
 				width: 90%;
--- a/src/private/tools/index.html.luan	Thu Oct 24 15:37:35 2024 -0600
+++ b/src/private/tools/index.html.luan	Thu Oct 24 21:43:44 2024 -0600
@@ -14,7 +14,6 @@
 <html>
 	<head>
 <%		head() %>
-		<title>Chat Private Tools</title>
 	</head>
 	<body>
 <%		header() %>
--- a/src/site.css	Thu Oct 24 15:37:35 2024 -0600
+++ b/src/site.css	Thu Oct 24 21:43:44 2024 -0600
@@ -14,6 +14,10 @@
 	text-decoration: underline;
 }
 
+input[type="submit"] {
+	cursor: pointer;
+}
+
 div[header] {
 	font-size: 14px;
 	background-color: #ddd;
@@ -27,3 +31,10 @@
 	margin-right: 3%;
 	margin-bottom: 2em;
 }
+
+label[prompt] {
+	display: block;
+	font-size: small;
+	margin-top: 2px;
+	margin-bottom: 4px;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/site.js	Thu Oct 24 21:43:44 2024 -0600
@@ -0,0 +1,76 @@
+'use strict';
+
+function ajax(url,postData,context) {
+	let request = new XMLHttpRequest();
+	let method = postData ? 'POST' : 'GET';
+	request.open( method, url );
+	if( postData )
+		request.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
+	request.onload = function() {
+		if( request.status !== 200 ) {
+			let err = 'ajax failed: ' + request.status;
+			if( request.responseText ) {
+				err += '\n' + request.responseText.trim();
+				document.write('<pre>'+request.responseText+'</pre>');
+			}
+			err += '\nurl = ' + url;
+			err += '\npage = ' + window.location;
+			ajax( '/error_log.js', 'err='+encodeURIComponent(err) );
+			return;
+		}
+		try {
+			eval( request.responseText );
+		} catch(e) {
+			console.log( request.responseText );
+			window.err = '\najax-url = ' + url;
+			throw e;
+		}
+	};
+	request.send(postData);
+}
+
+window.onerror = function(msg, url, line, col, error) {
+	if( !url )
+		return;
+	let err = msg;
+	err += '\nurl = ' + url;
+	if( url != window.location )
+		err += '\npage = ' + window.location;
+	err += '\nline = '+line;
+	if( col )
+		err += '\ncolumn = ' + col;
+	if( error ) {
+		if( error.stack )
+			err += '\nstack = ' + error.stack;
+		if( error.cause )
+			err += '\ncause= ' + error.cause;
+		if( error.fileName )
+			err += '\nfileName= ' + error.fileName;
+	}
+	if( window.err ) {
+		err += window.err;
+		window.err = null;
+	}
+	ajax( '/error_log.js', 'err='+encodeURIComponent(err) );
+};
+
+function ajaxForm(url,form) {
+	let post = '';
+	for( let i=0; i<form.length; i++ ) {
+		let input = form[i];
+		let name = input.name;
+		if( name === '' )
+			continue;
+		let type = input.type;
+		if( (type==='radio' || type==='checkbox') && !input.checked )
+			continue;
+		post += name + '=' + encodeURIComponent(input.value) + '&';
+	}
+	ajax(url,post,{form:form});
+}
+
+function logout() {
+	document.cookie = 'user=; Max-Age=0; path=/;';
+	document.cookie = 'password=; Max-Age=0; path=/;';
+	location = '/';
+}