changeset 2:78708fa556a0

add login
author Franklin Schmidt <fschmidt@gmail.com>
date Tue, 08 Jul 2025 15:55:34 -0600
parents 1c87f785eb42
children eee6d4f59811
files .hgignore src/chat.html.luan src/do_login.html.luan src/index.html.luan src/lib/Db.luan src/lib/Shared.luan src/lib/User.luan src/lib/Utils.luan src/lib/ai/Ai.luan src/login.html.luan src/login.js.luan src/login_sent.html.luan src/private/Config_sample.luan src/site.css src/site.js
diffstat 15 files changed, 456 insertions(+), 17 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Tue Jul 08 14:18:25 2025 -0600
+++ b/.hgignore	Tue Jul 08 15:55:34 2025 -0600
@@ -1,3 +1,5 @@
 syntax: glob
 
 err
+local/
+private/Config.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/chat.html.luan	Tue Jul 08 15:55:34 2025 -0600
@@ -0,0 +1,46 @@
+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 function get_ai_thread(ai_key)
+	return "thread"
+end
+
+return function()
+	local ai_key = "whatever"
+	local thread = get_ai_thread(ai_key)
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+	</head>
+	<body>
+<%		header() %>
+		<div content>
+			<h1>Chat</h1>
+			<div ai_container="<%=ai_key%>" >
+				<div flex>
+					<div scroll>
+						<h2>Let's chat</h2>
+						<div messages>
+						</div>
+					</div>
+					<div ask>
+						<textarea autofocus oninput="fixTextarea(event)" onkeydown="textareaKey('<%=ai_key%>',event)"></textarea>
+						<button onclick="askAi('<%=ai_key%>')" title="Send"><img src="/images/send.svg"></button>
+					</div>
+				</div>
+				<img waiting-ai-icon src="/images/spinner_green.gif">
+			</div>
+		</div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/do_login.html.luan	Tue Jul 08 15:55:34 2025 -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 User = require "site:/lib/User.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local header = Shared.header or error()
+
+
+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 lang="en">
+	<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
--- a/src/index.html.luan	Tue Jul 08 14:18:25 2025 -0600
+++ b/src/index.html.luan	Tue Jul 08 15:55:34 2025 -0600
@@ -8,34 +8,18 @@
 
 
 return function()
-	local ai_key = "whatever"
 	Io.stdout = Http.response.text_writer()
 %>
 <!doctype html>
 <html lang="en">
 	<head>
 <%		head() %>
-		<title>Lang</title>
-		<style>
-		</style>
 	</head>
 	<body>
 <%		header() %>
 		<div content>
 			<h1>Lang</h1>
-			<div ai_container="<%=ai_key%>" >
-				<div flex>
-					<div scroll>
-						<h2>Let's chat</h2>
-						<div messages></div>
-					</div>
-					<div ask>
-						<textarea autofocus oninput="fixTextarea(event)" onkeydown="textareaKey('<%=ai_key%>',event)"></textarea>
-						<button onclick="askAi('<%=ai_key%>')" title="Send"><img src="/images/send.svg"></button>
-					</div>
-				</div>
-				<img waiting-ai-icon src="/images/spinner_green.gif">
-			</div>
+			<p><a href="chat.html">chat</a></p>
 		</div>
 	</body>
 </html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/Db.luan	Tue Jul 08 15:55:34 2025 -0600
@@ -0,0 +1,27 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local uri = Io.uri or error()
+local Thread = require "luan:Thread.luan"
+local Time = require "luan:Time.luan"
+local Lucene = require "luan:lucene/Lucene.luan"
+local Http = require "luan:http/Http.luan"
+
+
+local dir = uri("site:/private/local/lucene")
+
+local Db = Lucene.index( dir, {
+	log_dir = uri("site:/private/local/lucene_log")
+	name = "lucene"
+	version = 1
+} )
+
+Db.indexed_fields.user_email = Lucene.type.lowercase
+
+Db.restore_from_log()
+
+if Http.is_serving then
+	Thread.schedule( Db.check, { delay=0, repeating_delay=Time.period{hours=1} } )
+end
+
+return Db
--- a/src/lib/Shared.luan	Tue Jul 08 14:18:25 2025 -0600
+++ b/src/lib/Shared.luan	Tue Jul 08 15:55:34 2025 -0600
@@ -1,6 +1,12 @@
 local Luan = require "luan:Luan.luan"
 local error = Luan.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 Config = require "site:/private/Config.luan"
 
 
 local Shared = {}
@@ -10,6 +16,7 @@
 function Shared.head()
 %>
 		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<title>Lang</title>
 		<style>
 			@import "/site.css?s=<%=started%>";
 		</style>
@@ -18,10 +25,35 @@
 end
 
 function Shared.header()
+	local user = current_user()
 %>
 		<div header>
+			<span>
+				<a href="/">Lang</a>
+			</span>
+			<span>
+<%	if user == nil then %>
+				<a href="/login.html">Login / Register</a>
+<%	else %>
+				<a href="/account.html"><%= user.email %></a>
+<%	end %>
+			</span>
 		</div>
 <%
 end
 
+local default_from = "Lang <lang@luan.software>"
+local send_mail0 = Mail.sender(Config.mail_server).send
+function Shared.send_mail(mail)
+	mail.From = mail.From or default_from
+	send_mail0(mail)
+end
+
+function Shared.send_mail_async(mail)
+	mail.From = mail.From or default_from
+	thread_run( function()
+		send_mail0(mail)
+	end )
+end
+
 return Shared
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/User.luan	Tue Jul 08 15:55:34 2025 -0600
@@ -0,0 +1,129 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.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 Http = require "luan:http/Http.luan"
+local Lucene = require "luan:lucene/Lucene.luan"
+local lucene_quote = Lucene.quote or error()
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+local Utils = require "site:/lib/Utils.luan"
+local base_url = Utils.base_url or error()
+
+
+local User = {}
+
+local function from_doc(doc)
+	doc.type == "user" or error "wrong type"
+	return User.new {
+		id = doc.id
+		email = doc.user_email
+		password = doc.password
+	}
+end
+
+local function to_doc(user)
+	return {
+		type = "user"
+		id = user.id
+		user_email = user.email or error()
+		password = user.password or error()
+	}
+end
+
+function User.new(user)
+
+	function user.save()
+		local doc = to_doc(user)
+		Db.save(doc)
+		user.id = doc.id
+	end
+
+	function user.login()
+		local id = to_string(user.id)
+		Http.response.set_persistent_cookie("user",id)
+		Http.response.set_persistent_cookie("password",user.password)
+		Http.request.cookies.user = id
+		Http.request.cookies.password = user.password or error()
+	end
+
+	function user.login_url()
+		return base_url().."/do_login.html?user="..user.id.."&password="..user.password
+	end
+
+	return user
+end
+
+local function get_by_id(id)
+	local doc = Db.get_document("id:"..id)
+	return doc and doc.type=="user" and from_doc(doc) or nil
+end
+User.get_by_id = get_by_id
+
+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
+
+local function get_by_email(email)
+	local doc = Db.get_document("user_email:"..lucene_quote(email))
+	return doc and from_doc(doc)
+end
+-- User.get_by_email = get_by_email
+
+function User.get_or_create_by_email(email)
+	return run_in_transaction( function()
+		local user = get_by_email(email)
+		if user == nil then
+			user = User.new{
+				email = email
+				password = new_password()
+				notify_email = email
+			}
+			user.save()
+		end
+		return user
+	end )
+end
+
+local function current()
+	local user = get_local_only(User,"current")
+	if user == nil then
+		local id = Http.request.cookies.user
+		local password = Http.request.cookies.password
+		if id == nil or password == nil then
+			user = "nil"
+		else
+			user = get_by_id(id)
+			if user == nil or user.password ~= password then
+				user = "nil"
+			end
+		end
+		set_local_only(User,"current",user)
+	end
+	return user ~= "nil" and user or nil
+end
+User.current = current
+
+return User
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/Utils.luan	Tue Jul 08 15:55:34 2025 -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/lib/ai/Ai.luan	Tue Jul 08 15:55:34 2025 -0600
@@ -0,0 +1,13 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+
+
+local Ai = {}
+
+local ai = "dummy"
+
+function Ai.require_ai(file)
+	return require("site:/private/lib/ai/"..ai.."/"..file)
+end
+
+return Ai
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/login.html.luan	Tue Jul 08 15:55:34 2025 -0600
@@ -0,0 +1,42 @@
+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 lang="en">
+	<head>
+<%		head() %>
+		<style>
+			input[name=email] {
+				width: 300px;
+				max-width: 100%;
+			}
+		</style>
+	</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	Tue Jul 08 15:55:34 2025 -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 send_mail_async = Shared.send_mail_async 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 = user.login_url()
+	send_mail_async {
+		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	Tue Jul 08 15:55:34 2025 -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 lang="en">
+	<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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/Config_sample.luan	Tue Jul 08 15:55:34 2025 -0600
@@ -0,0 +1,8 @@
+return {
+	mail_server = {
+		host = "mail.smtp2go.com"
+		port = 465
+		username = "xxx"
+		password = "xxx"
+	}
+}
--- a/src/site.css	Tue Jul 08 14:18:25 2025 -0600
+++ b/src/site.css	Tue Jul 08 15:55:34 2025 -0600
@@ -14,6 +14,14 @@
 	text-decoration: underline;
 }
 
+div[header] {
+	font-size: 14px;
+	background-color: #ddd;
+	padding: 8px 3%;
+	display: flex;
+	justify-content: space-between;
+}
+
 [content] {
 	margin-left: 3%;
 	margin-right: 3%;
--- a/src/site.js	Tue Jul 08 14:18:25 2025 -0600
+++ b/src/site.js	Tue Jul 08 15:55:34 2025 -0600
@@ -29,6 +29,47 @@
 	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 showWaitingAiIcon() {
 	document.querySelector('[waiting-ai-icon]').style.display = 'block';
 }